blob: 0ccf8a7cb3f887e7c60861d7540da87b66d3ce46 [file] [log] [blame]
// Copyright 2015 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 "components/browser_watcher/window_hang_monitor_win.h"
#include <memory>
#include <utility>
#include "base/base_paths.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/message_loop/message_loop.h"
#include "base/message_loop/message_loop_current.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/multiprocess_test.h"
#include "base/threading/thread.h"
#include "base/win/message_window.h"
#include "base/win/scoped_handle.h"
#include "base/win/win_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace browser_watcher {
namespace {
const char kChildReadPipeSwitch[] = "child_read_pipe";
const char kChildWritePipeSwitch[] = "child_write_pipe";
// Signals used for IPC between the monitor process and the monitor.
enum IPCSignal {
IPC_SIGNAL_INVALID,
IPC_SIGNAL_READY,
IPC_SIGNAL_TERMINATE_PROCESS,
IPC_SIGNAL_CREATE_MESSAGE_WINDOW,
IPC_SIGNAL_DELETE_MESSAGE_WINDOW,
IPC_SIGNAL_HANG_MESSAGE_WINDOW,
};
// Sends |ipc_signal| through the |write_pipe|.
bool SendPipeSignal(HANDLE write_pipe, IPCSignal ipc_signal) {
DWORD bytes_written = 0;
if (!WriteFile(write_pipe, &ipc_signal, sizeof(ipc_signal), &bytes_written,
nullptr))
return false;
return bytes_written == sizeof(ipc_signal);
}
// Blocks on |read_pipe| until a signal is received into |ipc_signal|.
bool WaitForPipeSignal(HANDLE read_pipe, IPCSignal* ipc_signal) {
CHECK(ipc_signal);
DWORD bytes_read = 0;
if (!ReadFile(read_pipe, ipc_signal, sizeof(*ipc_signal), &bytes_read,
nullptr))
return false;
return bytes_read == sizeof(*ipc_signal);
}
// Blocks on |read_pipe| until a signal is received and returns true if it
// matches |expected_ipc_signal|.
bool WaitForSpecificPipeSignal(HANDLE read_pipe,
IPCSignal expected_ipc_signal) {
IPCSignal received_signal = IPC_SIGNAL_INVALID;
return WaitForPipeSignal(read_pipe, &received_signal) &&
received_signal == expected_ipc_signal;
}
// Appends |handle| as a command line switch.
void AppendSwitchHandle(base::CommandLine* command_line,
std::string switch_name,
HANDLE handle) {
command_line->AppendSwitchASCII(
switch_name, base::UintToString(base::win::HandleToUint32(handle)));
}
// Retrieves the |handle| associated to |switch_name| from the command line.
HANDLE GetSwitchValueHandle(base::CommandLine* command_line,
std::string switch_name) {
std::string switch_string = command_line->GetSwitchValueASCII(switch_name);
unsigned int switch_uint = 0;
if (switch_string.empty() ||
!base::StringToUint(switch_string, &switch_uint)) {
DLOG(ERROR) << "Missing or invalid " << switch_name << " argument.";
return nullptr;
}
return reinterpret_cast<HANDLE>(switch_uint);
}
// An instance of this class lives in the monitored process and receives signals
// and executes their associated function.
class MonitoredProcessClient {
public:
MonitoredProcessClient()
: message_window_thread_("Message window thread"),
hang_event_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
read_pipe_.Set(GetSwitchValueHandle(command_line, kChildReadPipeSwitch));
write_pipe_.Set(GetSwitchValueHandle(command_line, kChildWritePipeSwitch));
}
~MonitoredProcessClient() {
if (message_window_thread_.IsRunning()) {
DeleteMessageWindow();
}
}
void RunEventLoop() {
bool running = true;
IPCSignal ipc_signal = IPC_SIGNAL_INVALID;
while (running) {
CHECK(WaitForPipeSignal(read_pipe_.Get(), &ipc_signal));
switch (ipc_signal) {
// The parent process should never send those.
case IPC_SIGNAL_INVALID:
case IPC_SIGNAL_READY:
CHECK(false);
break;
case IPC_SIGNAL_TERMINATE_PROCESS:
running = false;
break;
case IPC_SIGNAL_CREATE_MESSAGE_WINDOW:
CreateMessageWindow();
break;
case IPC_SIGNAL_DELETE_MESSAGE_WINDOW:
DeleteMessageWindow();
break;
case IPC_SIGNAL_HANG_MESSAGE_WINDOW:
HangMessageWindow();
break;
}
SendSignalToParent(IPC_SIGNAL_READY);
}
}
// Creates a thread then creates the message window on it.
void CreateMessageWindow() {
ASSERT_TRUE(message_window_thread_.StartWithOptions(
base::Thread::Options(base::MessageLoop::TYPE_UI, 0)));
bool succeeded = false;
base::WaitableEvent created(
base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
ASSERT_TRUE(message_window_thread_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MonitoredProcessClient::CreateMessageWindowInWorkerThread,
base::Unretained(this), &succeeded, &created)));
created.Wait();
ASSERT_TRUE(succeeded);
}
// Creates a thread then creates the message window on it.
void HangMessageWindow() {
message_window_thread_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&base::WaitableEvent::Wait, base::Unretained(&hang_event_)));
}
bool SendSignalToParent(IPCSignal ipc_signal) {
return SendPipeSignal(write_pipe_.Get(), ipc_signal);
}
private:
bool EmptyMessageCallback(UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) {
EXPECT_EQ(message_window_thread_.message_loop(),
base::MessageLoopCurrent::Get());
return false; // Pass through to DefWindowProc.
}
void CreateMessageWindowInWorkerThread(bool* success,
base::WaitableEvent* created) {
CHECK(created);
// As an alternative to checking if the name of the message window is the
// user data directory, the hang watcher verifies that the window name is an
// existing directory. DIR_CURRENT is used to meet this constraint.
base::FilePath existing_dir;
CHECK(base::PathService::Get(base::DIR_CURRENT, &existing_dir));
message_window_.reset(new base::win::MessageWindow);
*success = message_window_->CreateNamed(
base::Bind(&MonitoredProcessClient::EmptyMessageCallback,
base::Unretained(this)),
existing_dir.value());
created->Signal();
}
void DeleteMessageWindow() {
base::WaitableEvent deleted(
base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
message_window_thread_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MonitoredProcessClient::DeleteMessageWindowInWorkerThread,
base::Unretained(this), &deleted));
deleted.Wait();
message_window_thread_.Stop();
}
void DeleteMessageWindowInWorkerThread(base::WaitableEvent* deleted) {
CHECK(deleted);
message_window_.reset();
deleted->Signal();
}
// The thread that holds the message window.
base::Thread message_window_thread_;
std::unique_ptr<base::win::MessageWindow> message_window_;
// Event used to hang the message window.
base::WaitableEvent hang_event_;
// Anonymous pipe handles for IPC with the parent process.
base::win::ScopedHandle read_pipe_;
base::win::ScopedHandle write_pipe_;
DISALLOW_COPY_AND_ASSIGN(MonitoredProcessClient);
};
// The monitored process main function.
MULTIPROCESS_TEST_MAIN(MonitoredProcess) {
MonitoredProcessClient monitored_process_client;
CHECK(monitored_process_client.SendSignalToParent(IPC_SIGNAL_READY));
monitored_process_client.RunEventLoop();
return 0;
}
// Manages a WindowHangMonitor that lives on a background thread.
class HangMonitorThread {
public:
// Instantiates the background thread.
HangMonitorThread()
: event_(WindowHangMonitor::WINDOW_NOT_FOUND),
event_received_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED),
thread_("Hang monitor thread") {}
~HangMonitorThread() {
if (hang_monitor_)
DestroyWatcher();
}
// Starts the background thread and the monitor to observe Chrome message
// window for |process|. Blocks until the monitor has been initialized.
bool Start(base::Process process) {
if (!thread_.StartWithOptions(
base::Thread::Options(base::MessageLoop::TYPE_UI, 0))) {
return false;
}
base::WaitableEvent complete(
base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED);
if (!thread_.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&HangMonitorThread::StartupOnThread,
base::Unretained(this), std::move(process),
base::Unretained(&complete)))) {
return false;
}
complete.Wait();
return true;
}
// Returns true if a window event is detected within |timeout|.
bool TimedWaitForEvent(base::TimeDelta timeout) {
return event_received_.TimedWait(timeout);
}
// Blocks indefinitely for a window event and returns it.
WindowHangMonitor::WindowEvent WaitForEvent() {
event_received_.Wait();
return event_;
}
private:
// Destroys the monitor and stops the background thread. Blocks until the
// operation completes.
void DestroyWatcher() {
thread_.task_runner()->PostTask(
FROM_HERE, base::Bind(&HangMonitorThread::ShutdownOnThread,
base::Unretained(this)));
// This will block until the above-posted task completes.
thread_.Stop();
}
// Invoked when the monitor signals an event. Unblocks a call to
// TimedWaitForEvent or WaitForEvent.
void EventCallback(WindowHangMonitor::WindowEvent event) {
if (event_received_.IsSignaled())
ADD_FAILURE() << "Multiple calls to EventCallback.";
event_ = event;
event_received_.Signal();
}
// Initializes the WindowHangMonitor to observe the Chrome message window for
// |process|. Signals |complete| when done.
void StartupOnThread(base::Process process, base::WaitableEvent* complete) {
hang_monitor_.reset(new WindowHangMonitor(
base::TimeDelta::FromMilliseconds(100),
base::TimeDelta::FromMilliseconds(100),
base::Bind(&HangMonitorThread::EventCallback, base::Unretained(this))));
hang_monitor_->Initialize(std::move(process));
complete->Signal();
}
// Destroys the WindowHangMonitor.
void ShutdownOnThread() { hang_monitor_.reset(); }
// The detected event. Invalid if |event_received_| has not been signaled.
WindowHangMonitor::WindowEvent event_;
// Indicates that |event_| has been assigned in response to a callback from
// the WindowHangMonitor.
base::WaitableEvent event_received_;
// The WindowHangMonitor under test.
std::unique_ptr<WindowHangMonitor> hang_monitor_;
// The background thread.
base::Thread thread_;
DISALLOW_COPY_AND_ASSIGN(HangMonitorThread);
};
class WindowHangMonitorTest : public testing::Test {
public:
WindowHangMonitorTest() {}
~WindowHangMonitorTest() override {
// Close process if running.
monitored_process_.Terminate(1, false);
}
// Starts a child process that will be monitored. Handles to anonymous pipes
// are passed to the command line to provide a way to communicate with the
// child process. This function blocks until IPC_SIGNAL_READY is received.
bool StartMonitoredProcess() {
HANDLE child_read_pipe = nullptr;
HANDLE child_write_pipe = nullptr;
if (!CreatePipes(&child_read_pipe, &child_write_pipe))
return false;
base::CommandLine command_line =
base::GetMultiProcessTestChildBaseCommandLine();
command_line.AppendSwitchASCII(switches::kTestChildProcess,
"MonitoredProcess");
AppendSwitchHandle(&command_line, kChildReadPipeSwitch, child_read_pipe);
AppendSwitchHandle(&command_line, kChildWritePipeSwitch, child_write_pipe);
base::LaunchOptions options = {};
// TODO(brettw) bug 748258: Share only explicit handles.
options.inherit_mode = base::LaunchOptions::Inherit::kAll;
monitored_process_ = base::LaunchProcess(command_line, options);
if (!monitored_process_.IsValid())
return false;
return WaitForSignal(IPC_SIGNAL_READY);
}
void StartHangMonitor() {
monitor_thread_.Start(monitored_process_.Duplicate());
}
// Sends the |ipc_signal| to the child process and wait for a IPC_SIGNAL_READY
// response.
bool SendSignal(IPCSignal ipc_signal) {
if (!SendPipeSignal(write_pipe_.Get(), ipc_signal))
return false;
return WaitForSignal(IPC_SIGNAL_READY);
}
// Blocks until |ipc_signal| is received from the child process.
bool WaitForSignal(IPCSignal ipc_signal) {
return WaitForSpecificPipeSignal(read_pipe_.Get(), ipc_signal);
}
HangMonitorThread& monitor_thread() { return monitor_thread_; }
private:
// Creates pipes for IPC with the child process.
bool CreatePipes(HANDLE* child_read_pipe, HANDLE* child_write_pipe) {
CHECK(child_read_pipe);
CHECK(child_write_pipe);
SECURITY_ATTRIBUTES security_attributes = {
sizeof(SECURITY_ATTRIBUTES), nullptr, true /* inherit handles */};
HANDLE parent_read_pipe = nullptr;
if (!CreatePipe(&parent_read_pipe, child_write_pipe, &security_attributes,
0)) {
return false;
}
read_pipe_.Set(parent_read_pipe);
HANDLE parent_write_pipe = nullptr;
if (!CreatePipe(child_read_pipe, &parent_write_pipe, &security_attributes,
0)) {
return false;
}
write_pipe_.Set(parent_write_pipe);
return true;
}
// The thread that monitors the child process.
HangMonitorThread monitor_thread_;
// The process that is monitored.
base::Process monitored_process_;
// Anonymous pipe handles for IPC with the monitored process.
base::win::ScopedHandle read_pipe_;
base::win::ScopedHandle write_pipe_;
DISALLOW_COPY_AND_ASSIGN(WindowHangMonitorTest);
};
} // namespace
TEST_F(WindowHangMonitorTest, WindowNotFound) {
ASSERT_TRUE(StartMonitoredProcess());
StartHangMonitor();
ASSERT_TRUE(SendSignal(IPC_SIGNAL_TERMINATE_PROCESS));
EXPECT_EQ(WindowHangMonitor::WINDOW_NOT_FOUND,
monitor_thread().WaitForEvent());
}
TEST_F(WindowHangMonitorTest, WindowVanished) {
ASSERT_TRUE(StartMonitoredProcess());
ASSERT_TRUE(SendSignal(IPC_SIGNAL_CREATE_MESSAGE_WINDOW));
StartHangMonitor();
ASSERT_FALSE(monitor_thread().TimedWaitForEvent(
base::TimeDelta::FromMilliseconds(250)));
ASSERT_TRUE(SendSignal(IPC_SIGNAL_DELETE_MESSAGE_WINDOW));
EXPECT_EQ(WindowHangMonitor::WINDOW_VANISHED,
monitor_thread().WaitForEvent());
ASSERT_TRUE(SendSignal(IPC_SIGNAL_TERMINATE_PROCESS));
}
TEST_F(WindowHangMonitorTest, WindowHang) {
ASSERT_TRUE(StartMonitoredProcess());
ASSERT_TRUE(SendSignal(IPC_SIGNAL_CREATE_MESSAGE_WINDOW));
StartHangMonitor();
ASSERT_FALSE(monitor_thread().TimedWaitForEvent(
base::TimeDelta::FromMilliseconds(250)));
ASSERT_TRUE(SendSignal(IPC_SIGNAL_HANG_MESSAGE_WINDOW));
EXPECT_EQ(WindowHangMonitor::WINDOW_HUNG,
monitor_thread().WaitForEvent());
ASSERT_TRUE(SendSignal(IPC_SIGNAL_TERMINATE_PROCESS));
}
} // namespace browser_watcher