Commit d01da0dd authored by Greg Thompson's avatar Greg Thompson Committed by Commit Bot

Revise on-top window data collection for interative_ui_tests.

Tests in the interative_ui_tests fixture are known to timeout if there's
an always-on-top window on the desktop. This change introduces a few
improvements to the existing data collection strategy to help diagnose
the provenance of these always-on-top windows.

- Checks for always-on-top windows are moved out of the test child
  processes and up to the parent test proc. This is a more reliable
  place to log any info, as such log messages are more likely to make it
  into the main swarming log output. This is a result of the way
  GetTestOutputSnippet pulls what it thinks is the only relevant test
  output -- it drops messages emitted outside of the test body.

- Scans for always-on-top windows now take place before running any test
  in the shard and after any test that times out.

- A screen snapshot is saved if any always-on-top window is found, even
  in the case of dialogs that will be closed.

- An attempt is made to find and emit the command lines for all
  processes in the process hierarchy of the proc owning an always-on-top
  window.

- All details are emitted in a single log message. For example:

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; window class name: ConsoleWindowClass; subprocess command line: "".\interactive_ui_tests.exe" --brave-new-test-launcher --cfi-diag=0 --gtest_also_run_disabled_tests --gtest_filter=AutofillInteractiveTest.AutofillViaClick --single_process --snapshot-output-dir="e:\b\s\w\iomhwbd8" --test-launcher-bot-mode --test-launcher-output="C:\Users\CHROME~2\AppData\Local\Temp\scoped_dir6396_16240\results6396_26680\test_results.xml" --test-launcher-summary-output="e:\b\s\w\iomhwbd8\output.json" --user-data-dir="C:\Users\CHROME~2\AppData\Local\Temp\scoped_dir6396_16240\d6396_27306""; owning process lineage: (process_id: 2000, command_line: "C:\Windows\system32\cmd.exe /c ""C:\Users\chrome-bot\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\run_swarm_bot.bat" ""), (process_id: 1496, command_line: "C:\Windows\Explorer.EXE"); screen snapshot saved to file: "e:\b\s\w\iomhwbd8\ss_20171030114011_636.png";

BUG=764415
R=sky@chromium.org

Change-Id: I1aed3a41542680b197c7856e1f5d9ec9af6e3774
Reviewed-on: https://chromium-review.googlesource.com/737641Reviewed-by: default avatarScott Violet <sky@chromium.org>
Commit-Queue: Greg Thompson <grt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#513095}
parent d8ffc1ac
...@@ -493,6 +493,10 @@ if (!is_android) { ...@@ -493,6 +493,10 @@ if (!is_android) {
"base/interactive_test_utils_mac.mm", "base/interactive_test_utils_mac.mm",
"base/interactive_test_utils_win.cc", "base/interactive_test_utils_win.cc",
"base/interactive_ui_tests_main.cc", "base/interactive_ui_tests_main.cc",
"base/process_inspector_win.cc",
"base/process_inspector_win.h",
"base/save_desktop_snapshot_win.cc",
"base/save_desktop_snapshot_win.h",
"base/view_event_test_platform_part.h", "base/view_event_test_platform_part.h",
"base/view_event_test_platform_part_chromeos.cc", "base/view_event_test_platform_part_chromeos.cc",
"base/view_event_test_platform_part_default.cc", "base/view_event_test_platform_part_default.cc",
......
...@@ -11,180 +11,57 @@ ...@@ -11,180 +11,57 @@
#include <vector> #include <vector>
#include "base/command_line.h" #include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/numerics/safe_conversions.h"
#include "base/process/process.h" #include "base/process/process.h"
#include "base/run_loop.h" #include "chrome/test/base/process_inspector_win.h"
#include "base/strings/stringprintf.h" #include "chrome/test/base/save_desktop_snapshot_win.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
#include "ui/display/win/screen_win.h" #include "ui/display/win/screen_win.h"
#include "ui/gfx/codec/png_codec.h"
namespace { namespace {
constexpr char kDialogFoundBeforeTest[] = constexpr char kDialogFoundBeforeTest[] =
"There is an always on top dialog on the desktop. This was most likely " "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 " "caused by a previous test and may cause this test to fail. Trying to "
"close it."; "close it;";
constexpr char kDialogFoundPostTest[] = constexpr char kDialogFoundPostTest[] =
"There is an always on top dialog on the desktop after running this test. " "There is an always on top dialog on the desktop after this test timed "
"This was most likely caused by this test and may cause future tests to " "out. This was most likely caused by this test and may cause future tests "
"fail, trying to close it."; "to fail, trying to close it;";
constexpr char kWindowFoundBeforeTest[] = constexpr char kWindowFoundBeforeTest[] =
"There is an always on top window on the desktop before running the test. " "There is an always on top window on the desktop. This may have been "
"This may have been caused by a previous test and may cause this test to " "caused by a previous test and may cause this test to fail;";
"fail, class-name=";
constexpr char kWindowFoundPostTest[] = constexpr char kWindowFoundPostTest[] =
"There is an always on top window on the desktop after running the test. " "There is an always on top window on the desktop after this test timed "
"This may have been caused by this test or a previous test and may cause " "out. This may have been caused by this test or a previous test and may "
"flake, class-name="; "cause flakes;";
// A command line switch to specify the output directory into which snapshots // A command line switch to specify the output directory into which snapshots
// are to be saved in case an always-on-top window is found. // are to be saved in case an always-on-top window is found.
constexpr char kSnapshotOutputDir[] = "snapshot-output-dir"; constexpr char kSnapshotOutputDir[] = "snapshot-output-dir";
// A worker that captures a single frame from a webrtc::DesktopCapturer and then
// runs a callback when done.
class CaptureWorker : public webrtc::DesktopCapturer::Callback {
public:
CaptureWorker(std::unique_ptr<webrtc::DesktopCapturer> capturer,
base::Closure on_done)
: capturer_(std::move(capturer)), on_done_(std::move(on_done)) {
capturer_->Start(this);
capturer_->CaptureFrame();
}
// Returns the frame that was captured or null in case of failure.
std::unique_ptr<webrtc::DesktopFrame> TakeFrame() {
return std::move(frame_);
}
private:
// webrtc::DesktopCapturer::Callback:
void OnCaptureResult(webrtc::DesktopCapturer::Result result,
std::unique_ptr<webrtc::DesktopFrame> frame) override {
if (result == webrtc::DesktopCapturer::Result::SUCCESS)
frame_ = std::move(frame);
on_done_.Run();
}
std::unique_ptr<webrtc::DesktopCapturer> capturer_;
base::Closure on_done_;
std::unique_ptr<webrtc::DesktopFrame> frame_;
DISALLOW_COPY_AND_ASSIGN(CaptureWorker);
};
// Captures and returns a snapshot of the screen, or an empty bitmap in case of
// error.
SkBitmap CaptureScreen() {
auto options = webrtc::DesktopCaptureOptions::CreateDefault();
options.set_disable_effects(false);
options.set_allow_directx_capturer(true);
options.set_allow_use_magnification_api(false);
std::unique_ptr<webrtc::DesktopCapturer> capturer(
webrtc::DesktopCapturer::CreateScreenCapturer(options));
// Grab a single frame.
std::unique_ptr<webrtc::DesktopFrame> frame;
{
// While webrtc::DesktopCapturer seems to be synchronous, comments in its
// implementation seem to indicate that it may require a UI message loop on
// its thread.
base::MessageLoopForUI message_loop;
base::RunLoop run_loop;
CaptureWorker worker(std::move(capturer), run_loop.QuitClosure());
run_loop.Run();
frame = worker.TakeFrame();
}
if (!frame)
return SkBitmap();
// Create an image from the frame.
SkBitmap result;
result.allocN32Pixels(frame->size().width(), frame->size().height(), true);
memcpy(result.getAddr32(0, 0), frame->data(),
frame->size().width() * frame->size().height() *
webrtc::DesktopFrame::kBytesPerPixel);
return result;
}
// Saves a snapshot of the screen to a file in |output_dir|, returning the path
// to the file if created. An empty path is returned if no new snapshot is
// created.
base::FilePath SaveSnapshot(const base::FilePath& output_dir) {
// Create the output file.
base::Time::Exploded exploded;
base::Time::Now().LocalExplode(&exploded);
base::FilePath output_path(
output_dir.Append(base::FilePath(base::StringPrintf(
L"ss_%4d%02d%02d%02d%02d%02d_%03d.png", exploded.year, exploded.month,
exploded.day_of_month, exploded.hour, exploded.minute,
exploded.second, exploded.millisecond))));
base::File file(output_path, base::File::FLAG_CREATE |
base::File::FLAG_WRITE |
base::File::FLAG_SHARE_DELETE |
base::File::FLAG_CAN_DELETE_ON_CLOSE);
if (!file.IsValid()) {
if (file.error_details() == base::File::FILE_ERROR_EXISTS) {
LOG(INFO) << "Skipping screen snapshot since it is already present: "
<< output_path.BaseName();
} else {
LOG(ERROR) << "Failed to create snapshot output file \"" << output_path
<< "\" with error " << file.error_details();
}
return base::FilePath();
}
// Delete the output file in case of any error.
file.DeleteOnClose(true);
// Take the snapshot and encode it.
SkBitmap screen = CaptureScreen();
if (screen.drawsNothing()) {
LOG(ERROR) << "Failed to capture a frame of the screen for a snapshot.";
return base::FilePath();
}
std::vector<unsigned char> encoded;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(CaptureScreen(), false, &encoded)) {
LOG(ERROR) << "Failed to PNG encode screen snapshot.";
return base::FilePath();
}
// Write it to disk.
const int to_write = base::checked_cast<int>(encoded.size());
int written =
file.WriteAtCurrentPos(reinterpret_cast<char*>(encoded.data()), to_write);
if (written != to_write) {
LOG(ERROR) << "Failed to write entire snapshot to file";
return base::FilePath();
}
// Keep the output file.
file.DeleteOnClose(false);
return output_path;
}
// A window enumerator that searches for always-on-top windows. A snapshot of // A window enumerator that searches for always-on-top windows. A snapshot of
// the screen is saved if any unexpected on-top windows are found. // the screen is saved if any unexpected on-top windows are found.
class WindowEnumerator { class WindowEnumerator {
public: public:
explicit WindowEnumerator(RunType run_type); // |run_type| influences which log message is used. |child_command_line|, only
// specified when |run_type| is AFTER_TEST_TIMEOUT, is the command line of the
// child process that timed out.
WindowEnumerator(RunType run_type,
const base::CommandLine* child_command_line);
void Run(); void Run();
private: private:
// Properies of a running process.
struct ProcessProperties {
DWORD process_id;
base::string16 command_line;
};
// An EnumWindowsProc invoked by EnumWindows once for each window. // An EnumWindowsProc invoked by EnumWindows once for each window.
static BOOL CALLBACK OnWindowProc(HWND hwnd, LPARAM l_param); static BOOL CALLBACK OnWindowProc(HWND hwnd, LPARAM l_param);
...@@ -201,19 +78,31 @@ class WindowEnumerator { ...@@ -201,19 +78,31 @@ class WindowEnumerator {
// shell. // shell.
static bool IsShellWindowClass(const base::string16& class_name); static bool IsShellWindowClass(const base::string16& class_name);
// Returns the lineage of |process_id|; specifically, the pid and command line
// of |process_id| and each of its ancestors that are still running. Due to
// aggressive pid reuse on Windows, it's possible that the returned collection
// may contain misleading information. Since the goal of this method is to get
// useful data in the aggregate, some misleading info here and there is
// tolerable. If it proves to be intolerable, additional checks can be added
// to be sure that each ancestor is older that its child.
static std::vector<ProcessProperties> GetProcessLineage(DWORD process_id);
// Main processing function run for each window. // Main processing function run for each window.
BOOL OnWindow(HWND hwnd); BOOL OnWindow(HWND hwnd);
const base::FilePath output_dir_; const base::FilePath output_dir_;
const RunType run_type_; const RunType run_type_;
const base::CommandLine* const child_command_line_;
bool saved_snapshot_; bool saved_snapshot_;
DISALLOW_COPY_AND_ASSIGN(WindowEnumerator); DISALLOW_COPY_AND_ASSIGN(WindowEnumerator);
}; };
WindowEnumerator::WindowEnumerator(RunType run_type) WindowEnumerator::WindowEnumerator(RunType run_type,
const base::CommandLine* child_command_line)
: output_dir_(base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( : output_dir_(base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
kSnapshotOutputDir)), kSnapshotOutputDir)),
run_type_(run_type), run_type_(run_type),
child_command_line_(child_command_line),
saved_snapshot_(false) {} saved_snapshot_(false) {}
void WindowEnumerator::Run() { void WindowEnumerator::Run() {
...@@ -254,6 +143,34 @@ bool WindowEnumerator::IsShellWindowClass(const base::string16& class_name) { ...@@ -254,6 +143,34 @@ bool WindowEnumerator::IsShellWindowClass(const base::string16& class_name) {
class_name == L"Shell_SecondaryTrayWnd"; class_name == L"Shell_SecondaryTrayWnd";
} }
// static
std::vector<WindowEnumerator::ProcessProperties>
WindowEnumerator::GetProcessLineage(DWORD process_id) {
std::vector<ProcessProperties> properties;
while (true) {
base::Process process = base::Process::OpenWithAccess(
process_id, PROCESS_QUERY_INFORMATION | SYNCHRONIZE | PROCESS_VM_READ);
if (!process.IsValid())
break;
auto inspector = ProcessInspector::Create(process);
if (!inspector)
break;
// If PID reuse proves to be a problem, this would be a good point to add
// extra checks that |process| is older than the previously inspected
// process.
properties.push_back({process_id, inspector->command_line()});
DWORD parent_pid = inspector->GetParentPid();
if (process_id == parent_pid)
break;
process_id = parent_pid;
}
return properties;
}
BOOL WindowEnumerator::OnWindow(HWND hwnd) { BOOL WindowEnumerator::OnWindow(HWND hwnd) {
const BOOL kContinueIterating = TRUE; const BOOL kContinueIterating = TRUE;
...@@ -268,62 +185,80 @@ BOOL WindowEnumerator::OnWindow(HWND hwnd) { ...@@ -268,62 +185,80 @@ BOOL WindowEnumerator::OnWindow(HWND hwnd) {
if (IsShellWindowClass(class_name)) if (IsShellWindowClass(class_name))
return kContinueIterating; return kContinueIterating;
// Something unexpected was found. Save a snapshot of the screen if one hasn't // All other always-on-top windows may be problematic, but in theory tests
// already been saved and an output directory was specified. // should not be creating an always on top window that outlives the test.
if (!saved_snapshot_ && !output_dir_.empty()) { // Prepare details of the command line of the test that timed out (if
base::FilePath snapshot_file = SaveSnapshot(output_dir_); // provided), the process owning the window, and the location of a snapshot
// taken of the screen.
base::string16 details;
if (LOG_IS_ON(ERROR)) {
std::wostringstream sstream;
if (!IsSystemDialogClass(class_name))
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);
std::vector<ProcessProperties> process_properties =
GetProcessLineage(process_id);
if (!process_properties.empty()) {
sstream << " owning process lineage: ";
base::string16 sep;
for (const auto& prop : process_properties) {
sstream << sep << L"(process_id: " << prop.process_id
<< L", command_line: \"" << prop.command_line << "\")";
if (sep.empty())
sep = L", ";
}
sstream << ";";
}
if (!snapshot_file.empty()) { if (!snapshot_file.empty()) {
saved_snapshot_ = true; sstream << " screen snapshot saved to file: \"" << snapshot_file.value()
LOG(ERROR) << "Wrote snapshot to file " << snapshot_file; << "\";";
} }
details = sstream.str();
} }
// System dialogs may be present if a child process triggers an assert(), for // System dialogs may be present if a child process triggers an assert(), for
// example. // example.
if (IsSystemDialogClass(class_name)) { if (IsSystemDialogClass(class_name)) {
LOG(ERROR) << (run_type_ == RunType::BEFORE_TEST ? kDialogFoundBeforeTest LOG(ERROR) << (run_type_ == RunType::BEFORE_SHARD ? kDialogFoundBeforeTest
: kDialogFoundPostTest); : kDialogFoundPostTest)
<< details;
// We don't own the dialog, so we can't destroy it. CloseWindow() // 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, // results in iconifying the window. An alternative may be to focus it,
// then send return and wait for close. As we reboot machines running // 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 // interactive ui tests at least every 12 hours we're going with the
// simple for now. // simple for now.
CloseWindow(hwnd); CloseWindow(hwnd);
return kContinueIterating; } else {
} LOG(ERROR) << (run_type_ == RunType::BEFORE_SHARD ? kWindowFoundBeforeTest
: kWindowFoundPostTest)
// All other always-on-top windows may be problematic, but in theory tests << details;
// should not be creating an always on top window that outlives the test. Log
// attributes of the window in case there are problems.
DWORD process_id = 0;
DWORD thread_id = GetWindowThreadProcessId(hwnd, &process_id);
base::Process process = base::Process::Open(process_id);
base::string16 process_path(MAX_PATH, L'\0');
if (process.IsValid()) {
// It's possible that the actual process owning |hwnd| has gone away and
// that a new process using the same PID has appeared. If this turns out to
// be an issue, we could fetch the process start time here and compare it
// with the time just before getting |thread_id| above. This is likely
// overkill for diagnostic purposes.
DWORD str_len = process_path.size();
if (!::QueryFullProcessImageName(process.Handle(), 0, &process_path[0],
&str_len) ||
str_len >= MAX_PATH) {
str_len = 0;
}
process_path.resize(str_len);
} }
LOG(ERROR) << (run_type_ == RunType::BEFORE_TEST ? kWindowFoundBeforeTest
: kWindowFoundPostTest)
<< class_name << " process_id=" << process_id
<< " thread_id=" << thread_id << " process_path=" << process_path;
return kContinueIterating; return kContinueIterating;
} }
} // namespace } // namespace
void KillAlwaysOnTopWindows(RunType run_type) { void KillAlwaysOnTopWindows(RunType run_type,
WindowEnumerator(run_type).Run(); const base::CommandLine* child_command_line) {
WindowEnumerator(run_type, child_command_line).Run();
} }
...@@ -5,16 +5,23 @@ ...@@ -5,16 +5,23 @@
#ifndef CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_ #ifndef CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_
#define CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_ #define CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_
namespace base {
class CommandLine;
}
enum class RunType { enum class RunType {
// Indicates cleanup is happening before the test run. // Indicates cleanup is happening before sharded tests are run.
BEFORE_TEST, BEFORE_SHARD,
// Indicates cleanup is happening after the test run. // Indicates cleanup is happening after a test subprocess has timed out.
AFTER_TEST, AFTER_TEST_TIMEOUT,
}; };
// Logs if there are any always on top windows, and if one is a system dialog // Logs if there are any always on top windows, and if one is a system dialog
// closes it. // closes it. |child_command_line|, if non-null, is the command line of the
void KillAlwaysOnTopWindows(RunType run_type); // test subprocess that timed out.
void KillAlwaysOnTopWindows(
RunType run_type,
const base::CommandLine* child_command_line = nullptr);
#endif // CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_ #endif // CHROME_TEST_BASE_ALWAYS_ON_TOP_WINDOW_KILLER_WIN_H_
...@@ -51,7 +51,6 @@ class InteractiveUITestSuite : public ChromeTestSuite { ...@@ -51,7 +51,6 @@ class InteractiveUITestSuite : public ChromeTestSuite {
#elif defined(USE_AURA) #elif defined(USE_AURA)
#if defined(OS_WIN) #if defined(OS_WIN)
com_initializer_.reset(new base::win::ScopedCOMInitializer()); com_initializer_.reset(new base::win::ScopedCOMInitializer());
KillAlwaysOnTopWindows(RunType::BEFORE_TEST);
#endif #endif
#if defined(OS_LINUX) #if defined(OS_LINUX)
...@@ -70,7 +69,6 @@ class InteractiveUITestSuite : public ChromeTestSuite { ...@@ -70,7 +69,6 @@ class InteractiveUITestSuite : public ChromeTestSuite {
void Shutdown() override { void Shutdown() override {
#if defined(OS_WIN) #if defined(OS_WIN)
KillAlwaysOnTopWindows(RunType::AFTER_TEST);
com_initializer_.reset(); com_initializer_.reset();
#endif #endif
} }
...@@ -81,6 +79,37 @@ class InteractiveUITestSuite : public ChromeTestSuite { ...@@ -81,6 +79,37 @@ class InteractiveUITestSuite : public ChromeTestSuite {
#endif #endif
}; };
class InteractiveUITestLauncherDelegate : public ChromeTestLauncherDelegate {
public:
explicit InteractiveUITestLauncherDelegate(ChromeTestSuiteRunner* runner)
: ChromeTestLauncherDelegate(runner) {}
protected:
// content::TestLauncherDelegate:
void PreSharding() override {
ChromeTestLauncherDelegate::PreSharding();
#if defined(OS_WIN)
// Check for any always-on-top windows present before any tests are run.
// Take a snapshot if any are found and attempt to close any that are system
// dialogs.
KillAlwaysOnTopWindows(RunType::BEFORE_SHARD);
#endif
}
void OnTestTimedOut(const base::CommandLine& command_line) override {
#if defined(OS_WIN)
// Check for any always-on-top windows present before terminating the test.
// Take a snapshot if any are found and attempt to close any that are system
// dialogs.
KillAlwaysOnTopWindows(RunType::AFTER_TEST_TIMEOUT, &command_line);
#endif
ChromeTestLauncherDelegate::OnTestTimedOut(command_line);
}
private:
DISALLOW_COPY_AND_ASSIGN(InteractiveUITestLauncherDelegate);
};
class InteractiveUITestSuiteRunner : public ChromeTestSuiteRunner { class InteractiveUITestSuiteRunner : public ChromeTestSuiteRunner {
public: public:
int RunTestSuite(int argc, char** argv) override { int RunTestSuite(int argc, char** argv) override {
...@@ -106,6 +135,6 @@ int main(int argc, char** argv) { ...@@ -106,6 +135,6 @@ int main(int argc, char** argv) {
size_t parallel_jobs = 1U; size_t parallel_jobs = 1U;
InteractiveUITestSuiteRunner runner; InteractiveUITestSuiteRunner runner;
ChromeTestLauncherDelegate delegate(&runner); InteractiveUITestLauncherDelegate delegate(&runner);
return LaunchChromeTests(parallel_jobs, &delegate, argc, argv); return LaunchChromeTests(parallel_jobs, &delegate, argc, argv);
} }
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/base/process_inspector_win.h"
#include <winternl.h>
#include "base/process/process.h"
#include "base/win/windows_version.h"
namespace {
// Certain Windows types that depend on the word size of the OS (rather than the
// size of the current process) are defined here.
// PROCESS_BASIC_INFORMATION.
template <class Traits>
struct ProcessInformation {
using RemotePointer = typename Traits::RemotePointer;
DWORD exit_status() const { return static_cast<DWORD>(exit_status_); }
RemotePointer peb_base_address() const { return peb_base_address_; }
RemotePointer affinity_mask() const { return affinity_mask_; }
int base_priority() const { return static_cast<int>(base_priority_); }
DWORD unique_process_id() const { return static_cast<DWORD>(unique_pid_); }
DWORD inherited_from_unique_process_id() const {
return static_cast<DWORD>(inherited_from_unique_process_id_);
}
private:
RemotePointer exit_status_;
RemotePointer peb_base_address_;
RemotePointer affinity_mask_;
RemotePointer base_priority_;
RemotePointer unique_pid_;
RemotePointer inherited_from_unique_process_id_;
};
// A subset of a process's environment block.
template <class Traits>
struct ProcessExecutionBlock {
using RemotePointer = typename Traits::RemotePointer;
uint8_t InheritedAddressSpace;
uint8_t ReadImageFileExecOptions;
uint8_t BeingDebugged;
uint8_t ProcessFlags;
uint8_t Padding[4];
RemotePointer Mutant;
RemotePointer ImageBaseAddress;
RemotePointer Ldr;
RemotePointer ProcessParameters; // RtlUserProcessParameters
};
// UNICODE_STRING.
template <class Traits>
struct UnicodeString {
using RemotePointer = typename Traits::RemotePointer;
uint16_t Length;
uint16_t MaximumLength;
RemotePointer Buffer;
};
// CURDIR.
template <class Traits>
struct CurDir {
using RemotePointer = typename Traits::RemotePointer;
UnicodeString<Traits> DosPath;
RemotePointer Handle;
};
// RTL_USER_PROCESS_PARAMETERS.
template <class Traits>
struct RtlUserProcessParameters {
using RemotePointer = typename Traits::RemotePointer;
uint32_t MaximumLength;
uint32_t Length;
uint32_t Flags;
uint32_t DebugFlags;
RemotePointer ConsoleHandle;
uint32_t ConsoleFlags;
RemotePointer StandardInput;
RemotePointer StandardOutput;
RemotePointer StandardError;
CurDir<Traits> CurrentDirectory;
UnicodeString<Traits> DllPath;
UnicodeString<Traits> ImagePathName;
UnicodeString<Traits> CommandLine;
};
// A concrete ProcessInspector that can read from another process based on the
// architecture. |Traits| specifies traits based on the OS architecture.
template <class Traits>
class Inspector : public ProcessInspector {
public:
Inspector();
// ProcessInspector:
DWORD GetParentPid() const override;
const base::string16& command_line() const override;
private:
// ProcessInspector:
bool Inspect(const base::Process& process) override;
ProcessInformation<Traits> process_basic_information_;
ProcessExecutionBlock<Traits> peb_;
RtlUserProcessParameters<Traits> process_parameters_;
base::string16 command_line_;
DISALLOW_COPY_AND_ASSIGN(Inspector);
};
#if !defined(_WIN64)
// Traits for a 32-bit process running in WoW.
struct Wow64Traits {
// The name of the ntdll function to query process information.
static const char kQueryProcessInformationFunctionName[];
// The type of a pointer to the read process memory function.
using ReadMemoryFn =
NTSTATUS(NTAPI*)(HANDLE, uint64_t, void*, uint64_t, uint64_t*);
// An unsigned integer type matching the size of a pointer in the remote
// process.
using RemotePointer = uint64_t;
// Returns the function to read memory from a remote process.
static ReadMemoryFn GetReadMemoryFn() {
return reinterpret_cast<ReadMemoryFn>(::GetProcAddress(
::GetModuleHandle(L"ntdll.dll"), "NtWow64ReadVirtualMemory64"));
}
// Reads |buffer_size| bytes from |handle|'s process at |address| into
// |buffer| using |fn|. Returns true on success.
static bool ReadMemory(ReadMemoryFn fn,
HANDLE handle,
RemotePointer address,
void* buffer,
RemotePointer buffer_size) {
NTSTATUS status = fn(handle, address, buffer, buffer_size, nullptr);
if (NT_SUCCESS(status))
return true;
LOG(ERROR) << "Failed to read process memory with status " << std::hex
<< status << ".";
return false;
}
};
// static
constexpr char Wow64Traits::kQueryProcessInformationFunctionName[] =
"NtWow64QueryInformationProcess64";
#endif
// Traits for a 32-bit process running on 32-bit Windows, or a 64-bit process
// running on 64-bit Windows.
struct NormalTraits {
// The name of the ntdll function to query process information.
static const char kQueryProcessInformationFunctionName[];
// The type of a pointer to the read process memory function.
using ReadMemoryFn = decltype(&::ReadProcessMemory);
// An unsigned integer type matching the size of a pointer in the remote
// process.
using RemotePointer = uintptr_t;
// Returns the function to read memory from a remote process.
static ReadMemoryFn GetReadMemoryFn() { return &::ReadProcessMemory; }
// Reads |buffer_size| bytes from |handle|'s process at |address| into
// |buffer| using |fn|. Returns true on success.
static bool ReadMemory(ReadMemoryFn fn,
HANDLE handle,
RemotePointer address,
void* buffer,
RemotePointer buffer_size) {
BOOL result = fn(handle, reinterpret_cast<const void*>(address), buffer,
buffer_size, nullptr);
if (result)
return true;
PLOG(ERROR) << "Failed to read process memory";
return false;
}
};
// static
constexpr char NormalTraits::kQueryProcessInformationFunctionName[] =
"NtQueryInformationProcess";
template <class Traits>
Inspector<Traits>::Inspector() = default;
template <class Traits>
DWORD Inspector<Traits>::GetParentPid() const {
return process_basic_information_.inherited_from_unique_process_id();
}
template <class Traits>
const base::string16& Inspector<Traits>::command_line() const {
return command_line_;
}
template <class Traits>
bool Inspector<Traits>::Inspect(const base::Process& process) {
auto query_information_process_fn =
reinterpret_cast<decltype(&::NtQueryInformationProcess)>(
::GetProcAddress(::GetModuleHandle(L"ntdll.dll"),
Traits::kQueryProcessInformationFunctionName));
typename Traits::ReadMemoryFn read_memory_fn = Traits::GetReadMemoryFn();
if (!query_information_process_fn)
return false;
ULONG in_len = sizeof(process_basic_information_);
ULONG out_len = 0;
NTSTATUS status = query_information_process_fn(
process.Handle(), ProcessBasicInformation, &process_basic_information_,
in_len, &out_len);
if (NT_ERROR(status) || out_len != in_len)
return false;
if (!Traits::ReadMemory(read_memory_fn, process.Handle(),
process_basic_information_.peb_base_address(), &peb_,
sizeof(peb_))) {
return false;
}
if (!Traits::ReadMemory(read_memory_fn, process.Handle(),
peb_.ProcessParameters, &process_parameters_,
sizeof(process_parameters_))) {
return false;
}
if (process_parameters_.CommandLine.Length) {
command_line_.resize(process_parameters_.CommandLine.Length /
sizeof(wchar_t));
if (!Traits::ReadMemory(read_memory_fn, process.Handle(),
process_parameters_.CommandLine.Buffer,
&command_line_[0],
process_parameters_.CommandLine.Length)) {
command_line_.clear();
return false;
}
}
return true;
}
} // namespace
// static
std::unique_ptr<ProcessInspector> ProcessInspector::Create(
const base::Process& process) {
std::unique_ptr<ProcessInspector> inspector;
#if !defined(_WIN64)
using base::win::OSInfo;
if (OSInfo::GetInstance()->wow64_status() == OSInfo::WOW64_ENABLED)
inspector = std::make_unique<Inspector<Wow64Traits>>();
#endif
if (!inspector)
inspector = std::make_unique<Inspector<NormalTraits>>();
if (!inspector->Inspect(process))
inspector.reset();
return inspector;
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_TEST_BASE_PROCESS_INSPECTOR_WIN_H_
#define CHROME_TEST_BASE_PROCESS_INSPECTOR_WIN_H_
#include <windows.h>
#include <memory>
#include "base/macros.h"
#include "base/strings/string16.h"
namespace base {
class Process;
}
// An inspector that can read properties of a remote process.
class ProcessInspector {
public:
// Returns an instance that reads data from |process|, which must have been
// opened with at least PROCESS_VM_READ access rights. Returns null in case of
// any error.
static std::unique_ptr<ProcessInspector> Create(const base::Process& process);
virtual ~ProcessInspector() = default;
// Returns the parent process PID of the process.
virtual DWORD GetParentPid() const = 0;
// Returns the command line of the process.
virtual const base::string16& command_line() const = 0;
protected:
ProcessInspector() = default;
private:
// Inspects |process|, returning true if all inspections succeed.
virtual bool Inspect(const base::Process& process) = 0;
DISALLOW_COPY_AND_ASSIGN(ProcessInspector);
};
#endif // CHROME_TEST_BASE_PROCESS_INSPECTOR_WIN_H_
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/base/save_desktop_snapshot_win.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/files/file.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
#include "ui/gfx/codec/png_codec.h"
namespace {
// A worker that captures a single frame from a webrtc::DesktopCapturer and then
// runs a callback when done.
class CaptureWorker : public webrtc::DesktopCapturer::Callback {
public:
CaptureWorker(std::unique_ptr<webrtc::DesktopCapturer> capturer,
base::Closure on_done)
: capturer_(std::move(capturer)), on_done_(std::move(on_done)) {
capturer_->Start(this);
capturer_->CaptureFrame();
}
// Returns the frame that was captured or null in case of failure.
std::unique_ptr<webrtc::DesktopFrame> TakeFrame() {
return std::move(frame_);
}
private:
// webrtc::DesktopCapturer::Callback:
void OnCaptureResult(webrtc::DesktopCapturer::Result result,
std::unique_ptr<webrtc::DesktopFrame> frame) override {
if (result == webrtc::DesktopCapturer::Result::SUCCESS)
frame_ = std::move(frame);
on_done_.Run();
}
std::unique_ptr<webrtc::DesktopCapturer> capturer_;
base::Closure on_done_;
std::unique_ptr<webrtc::DesktopFrame> frame_;
DISALLOW_COPY_AND_ASSIGN(CaptureWorker);
};
// Captures and returns a snapshot of the screen, or an empty bitmap in case of
// error.
SkBitmap CaptureScreen() {
auto options = webrtc::DesktopCaptureOptions::CreateDefault();
options.set_disable_effects(false);
options.set_allow_directx_capturer(true);
options.set_allow_use_magnification_api(false);
std::unique_ptr<webrtc::DesktopCapturer> capturer(
webrtc::DesktopCapturer::CreateScreenCapturer(options));
// Grab a single frame.
std::unique_ptr<webrtc::DesktopFrame> frame;
{
// While webrtc::DesktopCapturer seems to be synchronous, comments in its
// implementation seem to indicate that it may require a UI message loop on
// its thread.
base::RunLoop run_loop;
CaptureWorker worker(std::move(capturer), run_loop.QuitClosure());
run_loop.Run();
frame = worker.TakeFrame();
}
if (!frame)
return SkBitmap();
// Create an image from the frame.
SkBitmap result;
result.allocN32Pixels(frame->size().width(), frame->size().height(), true);
memcpy(result.getAddr32(0, 0), frame->data(),
frame->size().width() * frame->size().height() *
webrtc::DesktopFrame::kBytesPerPixel);
return result;
}
} // namespace
base::FilePath SaveDesktopSnapshot(const base::FilePath& output_dir) {
// Create the output file.
base::Time::Exploded exploded;
base::Time::Now().LocalExplode(&exploded);
base::FilePath output_path(
output_dir.Append(base::FilePath(base::StringPrintf(
L"ss_%4d%02d%02d%02d%02d%02d_%03d.png", exploded.year, exploded.month,
exploded.day_of_month, exploded.hour, exploded.minute,
exploded.second, exploded.millisecond))));
base::File file(output_path, base::File::FLAG_CREATE |
base::File::FLAG_WRITE |
base::File::FLAG_SHARE_DELETE |
base::File::FLAG_CAN_DELETE_ON_CLOSE);
if (!file.IsValid()) {
if (file.error_details() == base::File::FILE_ERROR_EXISTS) {
LOG(INFO) << "Skipping screen snapshot since it is already present: "
<< output_path.BaseName();
} else {
LOG(ERROR) << "Failed to create snapshot output file \"" << output_path
<< "\" with error " << file.error_details();
}
return base::FilePath();
}
// Delete the output file in case of any error.
file.DeleteOnClose(true);
// Take the snapshot and encode it.
SkBitmap screen = CaptureScreen();
if (screen.drawsNothing()) {
LOG(ERROR) << "Failed to capture a frame of the screen for a snapshot.";
return base::FilePath();
}
std::vector<unsigned char> encoded;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(CaptureScreen(), false, &encoded)) {
LOG(ERROR) << "Failed to PNG encode screen snapshot.";
return base::FilePath();
}
// Write it to disk.
const int to_write = base::checked_cast<int>(encoded.size());
int written =
file.WriteAtCurrentPos(reinterpret_cast<char*>(encoded.data()), to_write);
if (written != to_write) {
LOG(ERROR) << "Failed to write entire snapshot to file";
return base::FilePath();
}
// Keep the output file.
file.DeleteOnClose(false);
return output_path;
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_TEST_BASE_SAVE_DESKTOP_SNAPSHOT_WIN_H_
#define CHROME_TEST_BASE_SAVE_DESKTOP_SNAPSHOT_WIN_H_
#include "base/files/file_path.h"
// Saves a snapshot of the desktop to a file in |output_dir|, returning the path
// to the file if created. An empty path is returned if no new snapshot is
// created.
base::FilePath SaveDesktopSnapshot(const base::FilePath& output_dir);
#endif // CHROME_TEST_BASE_SAVE_DESKTOP_SNAPSHOT_WIN_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment