// 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 "chrome/browser/process_singleton.h" #include #include "base/bind.h" #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/files/file_path.h" #include "base/files/scoped_temp_dir.h" #include "base/logging.h" #include "base/macros.h" #include "base/memory/scoped_ptr.h" #include "base/process/launch.h" #include "base/process/process.h" #include "base/process/process_handle.h" #include "base/strings/string16.h" #include "base/strings/stringprintf.h" #include "base/test/multiprocess_test.h" #include "base/win/scoped_handle.h" #include "base/win/wrapped_window_proc.h" #include "chrome/browser/chrome_process_finder_win.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_switches.h" #include "content/public/common/result_codes.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/multiprocess_func_list.h" namespace { const char kReadyEventNameFlag[] = "ready_event_name"; const char kContinueEventNameFlag[] = "continue_event_name"; const char kCreateWindowFlag[] = "create_window"; const int kErrorResultCode = 0x345; bool NotificationCallback(const base::CommandLine& command_line, const base::FilePath& current_directory) { // This is never called in this test, but would signal that the singleton // notification was successfully handled. NOTREACHED(); return true; } // The ProcessSingleton kills hung browsers with no visible windows without user // interaction. If a hung browser has visible UI, however, it asks the user // first. // This class is the very minimal implementation to create a visible window // in the hung test process to allow testing the latter path. class ScopedVisibleWindow { public: ScopedVisibleWindow() : class_(0), window_(NULL) {} ~ScopedVisibleWindow() { if (window_) ::DestroyWindow(window_); if (class_) ::UnregisterClass(reinterpret_cast(class_), NULL); } bool Create() { WNDCLASSEX wnd_cls = {0}; base::win::InitializeWindowClass( L"ProcessSingletonTest", base::win::WrappedWindowProc<::DefWindowProc>, 0, // style 0, // class_extra 0, // window_extra NULL, // cursor NULL, // background NULL, // menu_name NULL, // large_icon NULL, // small_icon &wnd_cls); class_ = ::RegisterClassEx(&wnd_cls); if (!class_) return false; window_ = ::CreateWindow(reinterpret_cast(class_), 0, WS_POPUP, 0, 0, 0, 0, 0, 0, NULL, 0); if (!window_) return false; ::ShowWindow(window_, SW_SHOW); DCHECK(window_); return true; } private: ATOM class_; HWND window_; DISALLOW_COPY_AND_ASSIGN(ScopedVisibleWindow); }; MULTIPROCESS_TEST_MAIN(ProcessSingletonTestProcessMain) { base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); base::FilePath user_data_dir = cmd_line->GetSwitchValuePath(switches::kUserDataDir); if (user_data_dir.empty()) return kErrorResultCode; base::string16 ready_event_name = cmd_line->GetSwitchValueNative(kReadyEventNameFlag); base::win::ScopedHandle ready_event( ::OpenEvent(EVENT_MODIFY_STATE, FALSE, ready_event_name.c_str())); if (!ready_event.IsValid()) return kErrorResultCode; base::string16 continue_event_name = cmd_line->GetSwitchValueNative(kContinueEventNameFlag); base::win::ScopedHandle continue_event( ::OpenEvent(SYNCHRONIZE, FALSE, continue_event_name.c_str())); if (!continue_event.IsValid()) return kErrorResultCode; ScopedVisibleWindow visible_window; if (cmd_line->HasSwitch(kCreateWindowFlag)) { if (!visible_window.Create()) return kErrorResultCode; } // Instantiate the process singleton. ProcessSingleton process_singleton(user_data_dir, base::Bind(&NotificationCallback)); if (!process_singleton.Create()) return kErrorResultCode; // Signal ready and block for the continue event. if (!::SetEvent(ready_event.Get())) return kErrorResultCode; if (::WaitForSingleObject(continue_event.Get(), INFINITE) != WAIT_OBJECT_0) return kErrorResultCode; return 0; } // This fixture is for testing the Windows platform-specific failure modes // of rendezvous, specifically the ones where the singleton-owning process // is hung. class ProcessSingletonTest : public base::MultiProcessTest { protected: enum WindowOption { WITH_WINDOW, NO_WINDOW }; ProcessSingletonTest() : window_option_(NO_WINDOW), should_kill_called_(false) {} void SetUp() override { ASSERT_NO_FATAL_FAILURE(base::MultiProcessTest::SetUp()); // Drop the process finder notification timeout to one second for testing. old_notification_timeout_ = chrome::SetNotificationTimeoutForTesting( base::TimeDelta::FromSeconds(1)); } void TearDown() override { chrome::SetNotificationTimeoutForTesting(old_notification_timeout_); if (browser_victim_.IsValid()) { EXPECT_TRUE(::SetEvent(continue_event_.Get())); EXPECT_TRUE(browser_victim_.WaitForExit(nullptr)); } base::MultiProcessTest::TearDown(); } void LaunchHungBrowserProcess(WindowOption window_option) { // Create a unique user data dir to rendezvous on. ASSERT_TRUE(user_data_dir_.CreateUniqueTempDir()); // Create the named "ready" event, this is unique to our process. ready_event_name_ = base::StringPrintf(L"ready-event-%d", base::GetCurrentProcId()); base::win::ScopedHandle ready_event( ::CreateEvent(NULL, TRUE, FALSE, ready_event_name_.c_str())); ASSERT_TRUE(ready_event.IsValid()); // Create the named "continue" event, this is unique to our process. continue_event_name_ = base::StringPrintf(L"continue-event-%d", base::GetCurrentProcId()); continue_event_.Set( ::CreateEvent(NULL, TRUE, FALSE, continue_event_name_.c_str())); ASSERT_TRUE(continue_event_.IsValid()); window_option_ = window_option; base::LaunchOptions options; options.start_hidden = true; browser_victim_ = SpawnChildWithOptions("ProcessSingletonTestProcessMain", options); // Wait for the ready event (or process exit). HANDLE handles[] = {ready_event.Get(), browser_victim_.Handle()}; // The wait should always return because either |ready_event| is signaled or // |browser_victim_| died unexpectedly or exited on error. DWORD result = ::WaitForMultipleObjects(arraysize(handles), handles, FALSE, INFINITE); ASSERT_EQ(WAIT_OBJECT_0, result); } base::CommandLine MakeCmdLine(const std::string& procname) override { base::CommandLine cmd_line = base::MultiProcessTest::MakeCmdLine(procname); cmd_line.AppendSwitchPath(switches::kUserDataDir, user_data_dir_.path()); cmd_line.AppendSwitchNative(kReadyEventNameFlag, ready_event_name_); cmd_line.AppendSwitchNative(kContinueEventNameFlag, continue_event_name_); if (window_option_ == WITH_WINDOW) cmd_line.AppendSwitch(kCreateWindowFlag); return cmd_line; } void PrepareTest(WindowOption window_option, bool allow_kill) { ASSERT_NO_FATAL_FAILURE(LaunchHungBrowserProcess(window_option)); // The ready event has been signalled - the process singleton is held by // the hung sub process. test_singleton_.reset(new ProcessSingleton( user_data_dir(), base::Bind(&NotificationCallback))); test_singleton_->OverrideShouldKillRemoteProcessCallbackForTesting( base::Bind(&ProcessSingletonTest::MockShouldKillRemoteProcess, base::Unretained(this), allow_kill)); } base::Process* browser_victim() { return &browser_victim_; } const base::FilePath& user_data_dir() const { return user_data_dir_.path(); } ProcessSingleton* test_singleton() const { return test_singleton_.get(); } bool should_kill_called() const { return should_kill_called_; } private: bool MockShouldKillRemoteProcess(bool allow_kill) { should_kill_called_ = true; return allow_kill; } base::string16 ready_event_name_; base::string16 continue_event_name_; WindowOption window_option_; base::ScopedTempDir user_data_dir_; base::Process browser_victim_; base::win::ScopedHandle continue_event_; scoped_ptr test_singleton_; base::TimeDelta old_notification_timeout_; bool should_kill_called_; DISALLOW_COPY_AND_ASSIGN(ProcessSingletonTest); }; } // namespace TEST_F(ProcessSingletonTest, KillsHungBrowserWithNoWindows) { ASSERT_NO_FATAL_FAILURE(PrepareTest(NO_WINDOW, false)); // As the hung browser has no visible window, it'll be killed without // user interaction. ProcessSingleton::NotifyResult notify_result = test_singleton()->NotifyOtherProcessOrCreate(); ASSERT_EQ(ProcessSingleton::PROFILE_IN_USE, notify_result); // The should-kill callback should not have been called, as the "browser" does // not have visible window. EXPECT_FALSE(should_kill_called()); // Verify that the hung browser has beem terminated with the // RESULT_CODE_HUNG exit code. int exit_code = 0; EXPECT_TRUE( browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code)); EXPECT_EQ(content::RESULT_CODE_HUNG, exit_code); } TEST_F(ProcessSingletonTest, DoesntKillWithoutUserPermission) { ASSERT_NO_FATAL_FAILURE(PrepareTest(WITH_WINDOW, false)); // As the hung browser has a visible window, this should query the user // before killing the hung process. ProcessSingleton::NotifyResult notify_result = test_singleton()->NotifyOtherProcessOrCreate(); ASSERT_EQ(ProcessSingleton::PROCESS_NOTIFIED, notify_result); // The should-kill callback should have been called, as the "browser" has a // visible window. EXPECT_TRUE(should_kill_called()); // Make sure the process hasn't been killed. int exit_code = 0; EXPECT_FALSE( browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code)); } TEST_F(ProcessSingletonTest, KillWithUserPermission) { ASSERT_NO_FATAL_FAILURE(PrepareTest(WITH_WINDOW, true)); // As the hung browser has a visible window, this should query the user // before killing the hung process. ProcessSingleton::NotifyResult notify_result = test_singleton()->NotifyOtherProcessOrCreate(); ASSERT_EQ(ProcessSingleton::PROFILE_IN_USE, notify_result); // The should-kill callback should have been called, as the "browser" has a // visible window. EXPECT_TRUE(should_kill_called()); // Verify that the hung browser has beem terminated with the // RESULT_CODE_HUNG exit code. int exit_code = 0; EXPECT_TRUE( browser_victim()->WaitForExitWithTimeout(base::TimeDelta(), &exit_code)); EXPECT_EQ(content::RESULT_CODE_HUNG, exit_code); }