diff options
author | jabdelmalek@google.com <jabdelmalek@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-02 23:02:58 +0000 |
---|---|---|
committer | jabdelmalek@google.com <jabdelmalek@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-02 23:02:58 +0000 |
commit | 65718d93adff4004bb6a1f386aadfd72a06dafac (patch) | |
tree | 7f4b9f26bb6f71e5651e671fa4b4c85ae245c8bd /chrome/browser | |
parent | ce3d76856f28537ffda058e6babc8f4f46d85eac (diff) | |
download | chromium_src-65718d93adff4004bb6a1f386aadfd72a06dafac.zip chromium_src-65718d93adff4004bb6a1f386aadfd72a06dafac.tar.gz chromium_src-65718d93adff4004bb6a1f386aadfd72a06dafac.tar.bz2 |
Convert the ProcessSingleton linux browser test to a unit test.
BUG=121574
Review URL: https://chromiumcodereview.appspot.com/10267018
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@135022 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/process_singleton.h | 26 | ||||
-rw-r--r-- | chrome/browser/process_singleton_linux.cc | 127 | ||||
-rw-r--r-- | chrome/browser/process_singleton_linux_uitest.cc | 324 | ||||
-rw-r--r-- | chrome/browser/process_singleton_linux_unittest.cc | 370 |
4 files changed, 466 insertions, 381 deletions
diff --git a/chrome/browser/process_singleton.h b/chrome/browser/process_singleton.h index 213999d..203a162 100644 --- a/chrome/browser/process_singleton.h +++ b/chrome/browser/process_singleton.h @@ -21,6 +21,7 @@ #include "base/file_path.h" #include "base/logging.h" #include "base/memory/ref_counted.h" +#include "base/process.h" #include "base/threading/non_thread_safe.h" #include "ui/gfx/native_widget_types.h" @@ -94,6 +95,9 @@ class ProcessSingleton : public base::NonThreadSafe { const CommandLine& command_line, const NotificationCallback& notification_callback, int timeout_seconds); + void OverrideCurrentPidForTesting(base::ProcessId pid); + void OverrideKillCallbackForTesting(const base::Callback<void(int)>& callback); + static void DisablePromptForTesting(); #endif // defined(OS_LINUX) || defined(OS_OPENBSD) #if defined(OS_WIN) && !defined(USE_AURA) @@ -168,6 +172,28 @@ class ProcessSingleton : public base::NonThreadSafe { HWND window_; // The HWND_MESSAGE window. bool is_virtualized_; // Stuck inside Microsoft Softricity VM environment. #elif defined(OS_LINUX) || defined(OS_OPENBSD) + // Return true if the given pid is one of our child processes. + // Assumes that the current pid is the root of all pids of the current + // instance. + bool IsSameChromeInstance(pid_t pid); + + // Extract the process's pid from a symbol link path and if it is on + // the same host, kill the process, unlink the lock file and return true. + // If the process is part of the same chrome instance, unlink the lock file + // and return true without killing it. + // If the process is on a different host, return false. + bool KillProcessByLockPath(); + + // Default function to kill a process, overridable by tests. + void KillProcess(int pid); + + // Allow overriding for tests. + base::ProcessId current_pid_; + + // Function to call when the other process is hung and needs to be killed. + // Allows overriding for tests. + base::Callback<void(int)> kill_callback_; + // Path in file system to the socket. FilePath socket_path_; diff --git a/chrome/browser/process_singleton_linux.cc b/chrome/browser/process_singleton_linux.cc index 2aa49d7..22d7a92 100644 --- a/chrome/browser/process_singleton_linux.cc +++ b/chrome/browser/process_singleton_linux.cc @@ -36,8 +36,6 @@ // process will be considered as hung for some reason. The second process then // retrieves the process id from the symbol link and kills it by sending // SIGKILL. Then the second process starts as normal. -// -// TODO(james.su@gmail.com): Add unittest for this class. #include "chrome/browser/process_singleton.h" @@ -84,7 +82,6 @@ #include "chrome/browser/ui/gtk/process_singleton_dialog.h" #endif #include "chrome/common/chrome_constants.h" -#include "chrome/common/chrome_switches.h" #include "content/public/browser/browser_thread.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" @@ -97,6 +94,7 @@ const int ProcessSingleton::kTimeoutInSeconds; namespace { +static bool g_disable_prompt; const char kStartToken[] = "START"; const char kACKToken[] = "ACK"; const char kShutdownToken[] = "SHUTDOWN"; @@ -307,13 +305,13 @@ void DisplayProfileInUseError(const std::string& lock_path, WideToUTF16(base::SysNativeMBToWide(lock_path)), l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); LOG(ERROR) << base::SysWideToNativeMB(UTF16ToWide(error)).c_str(); + if (!g_disable_prompt) { #if defined(TOOLKIT_GTK) - if (!CommandLine::ForCurrentProcess()->HasSwitch( - switches::kNoProcessSingletonDialog)) ProcessSingletonDialog::ShowAndRun(UTF16ToUTF8(error)); #else - NOTIMPLEMENTED(); + NOTIMPLEMENTED(); #endif + } } bool IsChromeProcess(pid_t pid) { @@ -323,53 +321,6 @@ bool IsChromeProcess(pid_t pid) { FilePath(chrome::kBrowserProcessExecutableName)); } -// Return true if the given pid is one of our child processes. -// Assumes that the current pid is the root of all pids of the current instance. -bool IsSameChromeInstance(pid_t pid) { - pid_t cur_pid = base::GetCurrentProcId(); - while (pid != cur_pid) { - pid = base::GetParentProcessId(pid); - if (pid < 0) - return false; - if (!IsChromeProcess(pid)) - return false; - } - return true; -} - -// Extract the process's pid from a symbol link path and if it is on -// the same host, kill the process, unlink the lock file and return true. -// If the process is part of the same chrome instance, unlink the lock file and -// return true without killing it. -// If the process is on a different host, return false. -bool KillProcessByLockPath(const FilePath& path) { - std::string hostname; - int pid; - ParseLockPath(path, &hostname, &pid); - - if (!hostname.empty() && hostname != net::GetHostName()) { - DisplayProfileInUseError(path.value(), hostname, pid); - return false; - } - UnlinkPath(path); - - if (IsSameChromeInstance(pid)) - return true; - - if (pid > 0) { - // TODO(james.su@gmail.com): Is SIGKILL ok? - int rv = kill(static_cast<base::ProcessHandle>(pid), SIGKILL); - // ESRCH = No Such Process (can happen if the other process is already in - // progress of shutting down and finishes before we try to kill it). - DCHECK(rv == 0 || errno == ESRCH) << "Error killing process: " - << safe_strerror(errno); - return true; - } - - LOG(ERROR) << "Failed to extract pid from path: " << path.value(); - return true; -} - // A helper class to hold onto a socket. class ScopedSocket { public: @@ -741,10 +692,14 @@ void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK( ProcessSingleton::ProcessSingleton(const FilePath& user_data_dir) : locked_(false), foreground_window_(NULL), + current_pid_(base::GetCurrentProcId()), ALLOW_THIS_IN_INITIALIZER_LIST(watcher_(new LinuxWatcher(this))) { socket_path_ = user_data_dir.Append(chrome::kSingletonSocketFilename); lock_path_ = user_data_dir.Append(chrome::kSingletonLockFilename); cookie_path_ = user_data_dir.Append(chrome::kSingletonCookieFilename); + + kill_callback_ = base::Bind(&ProcessSingleton::KillProcess, + base::Unretained(this)); } ProcessSingleton::~ProcessSingleton() { @@ -807,7 +762,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout( if (retries == timeout_seconds) { // Retries failed. Kill the unresponsive chrome process and continue. - if (!kill_unresponsive || !KillProcessByLockPath(lock_path_)) + if (!kill_unresponsive || !KillProcessByLockPath()) return PROFILE_IN_USE; return PROCESS_NONE; } @@ -838,7 +793,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout( // Send the message if (!WriteToSocket(socket.fd(), to_send.data(), to_send.length())) { // Try to kill the other process, because it might have been dead. - if (!kill_unresponsive || !KillProcessByLockPath(lock_path_)) + if (!kill_unresponsive || !KillProcessByLockPath()) return PROFILE_IN_USE; return PROCESS_NONE; } @@ -854,7 +809,7 @@ ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout( // Failed to read ACK, the other process might have been frozen. if (len <= 0) { - if (!kill_unresponsive || !KillProcessByLockPath(lock_path_)) + if (!kill_unresponsive || !KillProcessByLockPath()) return PROFILE_IN_USE; return PROCESS_NONE; } @@ -908,6 +863,19 @@ ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate( return LOCK_ERROR; } +void ProcessSingleton::OverrideCurrentPidForTesting(base::ProcessId pid) { + current_pid_ = pid; +} + +void ProcessSingleton::OverrideKillCallbackForTesting( + const base::Callback<void(int)>& callback) { + kill_callback_ = callback; +} + +void ProcessSingleton::DisablePromptForTesting() { + g_disable_prompt = true; +} + bool ProcessSingleton::Create( const NotificationCallback& notification_callback) { int sock; @@ -919,7 +887,7 @@ bool ProcessSingleton::Create( "%s%c%u", net::GetHostName().c_str(), kLockDelimiter, - base::GetCurrentProcId())); + current_pid_)); // Create symbol link before binding the socket, to ensure only one instance // can have the socket open. @@ -984,3 +952,48 @@ void ProcessSingleton::Cleanup() { UnlinkPath(cookie_path_); UnlinkPath(lock_path_); } + +bool ProcessSingleton::IsSameChromeInstance(pid_t pid) { + pid_t cur_pid = current_pid_; + while (pid != cur_pid) { + pid = base::GetParentProcessId(pid); + if (pid < 0) + return false; + if (!IsChromeProcess(pid)) + return false; + } + return true; +} + +bool ProcessSingleton::KillProcessByLockPath() { + std::string hostname; + int pid; + ParseLockPath(lock_path_, &hostname, &pid); + + if (!hostname.empty() && hostname != net::GetHostName()) { + DisplayProfileInUseError(lock_path_.value(), hostname, pid); + return false; + } + UnlinkPath(lock_path_); + + if (IsSameChromeInstance(pid)) + return true; + + if (pid > 0) { + kill_callback_.Run(pid); + return true; + } + + LOG(ERROR) << "Failed to extract pid from path: " << lock_path_.value(); + return true; +} + +void ProcessSingleton::KillProcess(int pid) { + // TODO(james.su@gmail.com): Is SIGKILL ok? + int rv = kill(static_cast<base::ProcessHandle>(pid), SIGKILL); + // ESRCH = No Such Process (can happen if the other process is already in + // progress of shutting down and finishes before we try to kill it). + DCHECK(rv == 0 || errno == ESRCH) << "Error killing process: " + << safe_strerror(errno); +} + diff --git a/chrome/browser/process_singleton_linux_uitest.cc b/chrome/browser/process_singleton_linux_uitest.cc deleted file mode 100644 index d09b3fc..0000000 --- a/chrome/browser/process_singleton_linux_uitest.cc +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) 2012 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 <sys/types.h> -#include <sys/wait.h> -#include <signal.h> -#include <unistd.h> -#include <vector> -#include <string> - -#include "base/bind.h" -#include "base/command_line.h" -#include "base/eintr_wrapper.h" -#include "base/file_path.h" -#include "base/path_service.h" -#include "base/stringprintf.h" -#include "base/test/test_timeouts.h" -#include "base/threading/thread.h" -#include "base/utf_string_conversions.h" -#include "chrome/common/chrome_constants.h" -#include "chrome/common/chrome_paths.h" -#include "chrome/common/chrome_switches.h" -#include "chrome/test/base/chrome_process_util.h" -#include "chrome/test/ui/ui_test.h" -#include "net/base/net_util.h" -#include "testing/gtest/include/gtest/gtest.h" - -namespace { - -bool UnexpectedNotificationCallback(const CommandLine& command_line, - const FilePath& current_directory) { - ADD_FAILURE() << "This callback should never be invoked because the active " - << "ProcessSingleton is that of the Browser process hosted by " - << "UITest."; - return false; -} - -class ProcessSingletonLinuxTest : public UITest { - public: - virtual void SetUp() { - UITest::SetUp(); - lock_path_ = user_data_dir().Append(chrome::kSingletonLockFilename); - socket_path_ = user_data_dir().Append(chrome::kSingletonSocketFilename); - cookie_path_ = user_data_dir().Append(chrome::kSingletonCookieFilename); - } - - virtual void TearDown() { - UITest::TearDown(); - - // Check that the test cleaned up after itself. - struct stat statbuf; - bool lock_exists = lstat(lock_path_.value().c_str(), &statbuf) == 0; - EXPECT_FALSE(lock_exists); - - if (lock_exists) { - // Unlink to prevent failing future tests if the lock still exists. - EXPECT_EQ(unlink(lock_path_.value().c_str()), 0); - } - } - - FilePath lock_path_; - FilePath socket_path_; - FilePath cookie_path_; -}; - -ProcessSingleton* CreateProcessSingleton() { - FilePath user_data_dir; - PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); - - return new ProcessSingleton(user_data_dir); -} - -CommandLine CommandLineForUrl(const std::string& url) { - // Hack: mutate the current process's command line so we don't show a dialog. - // Note that this only works if we have no loose values on the command line, - // but that's fine for unit tests. In a UI test we disable error dialogs - // when spawning Chrome, but this test hits the ProcessSingleton directly. - CommandLine* cmd_line = CommandLine::ForCurrentProcess(); - if (!cmd_line->HasSwitch(switches::kNoProcessSingletonDialog)) - cmd_line->AppendSwitch(switches::kNoProcessSingletonDialog); - - CommandLine new_cmd_line(*cmd_line); - new_cmd_line.AppendArg(url); - return new_cmd_line; -} - -// A helper method to call ProcessSingleton::NotifyOtherProcess(). -// |url| will be added to CommandLine for current process, so that it can be -// sent to browser process by ProcessSingleton::NotifyOtherProcess(). -ProcessSingleton::NotifyResult NotifyOtherProcess(const std::string& url, - int timeout_ms) { - scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); - return process_singleton->NotifyOtherProcessWithTimeout( - CommandLineForUrl(url), timeout_ms / 1000, true); -} - -// A helper method to call ProcessSingleton::NotifyOtherProcessOrCreate(). -// |url| will be added to CommandLine for current process, so that it can be -// sent to browser process by ProcessSingleton::NotifyOtherProcessOrCreate(). -ProcessSingleton::NotifyResult NotifyOtherProcessOrCreate( - const std::string& url, - int timeout_ms) { - scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); - return process_singleton->NotifyOtherProcessWithTimeoutOrCreate( - CommandLineForUrl(url), - base::Bind(&UnexpectedNotificationCallback), - timeout_ms / 1000); -} - -} // namespace - -// Test if the socket file and symbol link created by ProcessSingletonLinux -// are valid. When running this test, the ProcessSingleton object is already -// initiated by UITest. So we just test against this existing object. -// This test is flaky as per http://crbug.com/74554. -TEST_F(ProcessSingletonLinuxTest, DISABLED_CheckSocketFile) { - struct stat statbuf; - ASSERT_EQ(0, lstat(lock_path_.value().c_str(), &statbuf)); - ASSERT_TRUE(S_ISLNK(statbuf.st_mode)); - char buf[PATH_MAX]; - ssize_t len = readlink(lock_path_.value().c_str(), buf, PATH_MAX); - ASSERT_GT(len, 0); - - ASSERT_EQ(0, lstat(socket_path_.value().c_str(), &statbuf)); - ASSERT_TRUE(S_ISLNK(statbuf.st_mode)); - - len = readlink(socket_path_.value().c_str(), buf, PATH_MAX); - ASSERT_GT(len, 0); - FilePath socket_target_path = FilePath(std::string(buf, len)); - - ASSERT_EQ(0, lstat(socket_target_path.value().c_str(), &statbuf)); - ASSERT_TRUE(S_ISSOCK(statbuf.st_mode)); - - len = readlink(cookie_path_.value().c_str(), buf, PATH_MAX); - ASSERT_GT(len, 0); - std::string cookie(buf, len); - - FilePath remote_cookie_path = socket_target_path.DirName(). - Append(chrome::kSingletonCookieFilename); - len = readlink(remote_cookie_path.value().c_str(), buf, PATH_MAX); - ASSERT_GT(len, 0); - EXPECT_EQ(cookie, std::string(buf, len)); -} - -#if defined(OS_LINUX) && defined(TOOLKIT_VIEWS) -// The following tests in linux/view does not pass without a window manager, -// which is true in build/try bots. -// See http://crbug.com/30953. -#define NotifyOtherProcessSuccess FAILS_NotifyOtherProcessSuccess -#define NotifyOtherProcessHostChanged FAILS_NotifyOtherProcessHostChanged -#endif - -// TODO(james.su@gmail.com): port following tests to Windows. -// Test success case of NotifyOtherProcess(). -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessSuccess) { - std::string url("about:blank"); - int original_tab_count = GetTabCount(); - - EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, - NotifyOtherProcess(url, TestTimeouts::action_timeout_ms())); - EXPECT_EQ(original_tab_count + 1, GetTabCount()); - EXPECT_EQ(url, GetActiveTabURL().spec()); -} - -// Test failure case of NotifyOtherProcess(). -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessFailure) { - base::ProcessId pid = browser_process_id(); - - ASSERT_GE(pid, 1); - - // Block the browser process, then it'll be killed by - // ProcessSingleton::NotifyOtherProcess(). - kill(pid, SIGSTOP); - - // Wait to make sure the browser process is actually stopped. - // It's necessary when running with valgrind. - EXPECT_GE(HANDLE_EINTR(waitpid(pid, 0, WUNTRACED)), 0); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROCESS_NONE, - NotifyOtherProcess(url, TestTimeouts::action_timeout_ms())); - - // Wait for a while to make sure the browser process is actually killed. - int exit_code = 0; - ASSERT_TRUE(launcher_->WaitForBrowserProcessToQuit( - TestTimeouts::action_max_timeout_ms(), &exit_code)); - EXPECT_EQ(-1, exit_code); // Expect unclean shutdown. -} - -// Test that we don't kill ourselves by accident if a lockfile with the same pid -// happens to exist. -// TODO(mattm): This doesn't really need to be a uitest. (We don't use the -// uitest created browser process, but we do use some uitest provided stuff like -// the user_data_dir and the NotifyOtherProcess function in this file, which -// would have to be duplicated or shared if this test was moved into a -// unittest.) -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessNoSuicide) { - // Replace lockfile with one containing our own pid. - EXPECT_EQ(0, unlink(lock_path_.value().c_str())); - std::string symlink_content = base::StringPrintf( - "%s%c%u", - net::GetHostName().c_str(), - '-', - base::GetCurrentProcId()); - EXPECT_EQ(0, symlink(symlink_content.c_str(), lock_path_.value().c_str())); - - // Remove socket so that we will not be able to notify the existing browser. - EXPECT_EQ(0, unlink(socket_path_.value().c_str())); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROCESS_NONE, - NotifyOtherProcess(url, TestTimeouts::action_timeout_ms())); - // If we've gotten to this point without killing ourself, the test succeeded. -} - -// Test that we can still notify a process on the same host even after the -// hostname changed. -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessHostChanged) { - EXPECT_EQ(0, unlink(lock_path_.value().c_str())); - EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); - - int original_tab_count = GetTabCount(); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, - NotifyOtherProcess(url, TestTimeouts::action_timeout_ms())); - EXPECT_EQ(original_tab_count + 1, GetTabCount()); - EXPECT_EQ(url, GetActiveTabURL().spec()); -} - -// Test that we fail when lock says process is on another host and we can't -// notify it over the socket. -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessDifferingHost) { - base::ProcessId pid = browser_process_id(); - - ASSERT_GE(pid, 1); - - // Kill the browser process, so that it does not respond on the socket. - kill(pid, SIGKILL); - // Wait for a while to make sure the browser process is actually killed. - int exit_code = 0; - ASSERT_TRUE(launcher_->WaitForBrowserProcessToQuit( - TestTimeouts::action_max_timeout_ms(), &exit_code)); - EXPECT_EQ(-1, exit_code); // Expect unclean shutdown. - - EXPECT_EQ(0, unlink(lock_path_.value().c_str())); - EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, - NotifyOtherProcess(url, TestTimeouts::action_timeout_ms())); - - ASSERT_EQ(0, unlink(lock_path_.value().c_str())); -} - -// Test that we fail when lock says process is on another host and we can't -// notify it over the socket. -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessOrCreate_DifferingHost) { - base::ProcessId pid = browser_process_id(); - - ASSERT_GE(pid, 1); - - // Kill the browser process, so that it does not respond on the socket. - kill(pid, SIGKILL); - // Wait for a while to make sure the browser process is actually killed. - int exit_code = 0; - ASSERT_TRUE(launcher_->WaitForBrowserProcessToQuit( - TestTimeouts::action_max_timeout_ms(), &exit_code)); - EXPECT_EQ(-1, exit_code); // Expect unclean shutdown. - - EXPECT_EQ(0, unlink(lock_path_.value().c_str())); - EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, - NotifyOtherProcessOrCreate(url, TestTimeouts::action_timeout_ms())); - - ASSERT_EQ(0, unlink(lock_path_.value().c_str())); -} - -// Test that Create fails when another browser is using the profile directory. -TEST_F(ProcessSingletonLinuxTest, CreateFailsWithExistingBrowser) { - scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); - EXPECT_FALSE(process_singleton->Create( - base::Bind(&UnexpectedNotificationCallback))); -} - -// Test that Create fails when another browser is using the profile directory -// but with the old socket location. -TEST_F(ProcessSingletonLinuxTest, CreateChecksCompatibilitySocket) { - scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); - - // Do some surgery so as to look like the old configuration. - char buf[PATH_MAX]; - ssize_t len = readlink(socket_path_.value().c_str(), buf, sizeof(buf)); - ASSERT_GT(len, 0); - FilePath socket_target_path = FilePath(std::string(buf, len)); - ASSERT_EQ(0, unlink(socket_path_.value().c_str())); - ASSERT_EQ(0, rename(socket_target_path.value().c_str(), - socket_path_.value().c_str())); - ASSERT_EQ(0, unlink(cookie_path_.value().c_str())); - - EXPECT_FALSE(process_singleton->Create( - base::Bind(&UnexpectedNotificationCallback))); -} - -// Test that we fail when lock says process is on another host and we can't -// notify it over the socket before of a bad cookie. -TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessOrCreate_BadCookie) { - // Change the cookie. - EXPECT_EQ(0, unlink(cookie_path_.value().c_str())); - EXPECT_EQ(0, symlink("INCORRECTCOOKIE", cookie_path_.value().c_str())); - - // Also change the hostname, so the remote does not retry. - EXPECT_EQ(0, unlink(lock_path_.value().c_str())); - EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); - - std::string url("about:blank"); - EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, - NotifyOtherProcessOrCreate(url, TestTimeouts::action_timeout_ms())); -} diff --git a/chrome/browser/process_singleton_linux_unittest.cc b/chrome/browser/process_singleton_linux_unittest.cc new file mode 100644 index 0000000..ede5915 --- /dev/null +++ b/chrome/browser/process_singleton_linux_unittest.cc @@ -0,0 +1,370 @@ +// Copyright (c) 2012 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 <sys/types.h> +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> +#include <vector> +#include <string> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/file_path.h" +#include "base/message_loop.h" +#include "base/process_util.h" +#include "base/scoped_temp_dir.h" +#include "base/stringprintf.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/test_timeouts.h" +#include "base/test/thread_test_helper.h" +#include "base/threading/thread.h" +#include "chrome/common/chrome_constants.h" +#include "content/test/test_browser_thread.h" +#include "net/base/net_util.h" +#include "testing/gtest/include/gtest/gtest.h" + + +namespace { + +bool NotificationCallback(const CommandLine& command_line, + const FilePath& current_directory) { + return true; +} + +class ProcessSingletonLinuxTest : public testing::Test { + public: + ProcessSingletonLinuxTest() + : kill_callbacks_(0), + io_thread_(content::BrowserThread::IO), + wait_event_(true, false), + signal_event_(true, false), + process_singleton_on_thread_(NULL) { + io_thread_.StartIOThread(); + } + + virtual void SetUp() { + testing::Test::SetUp(); + + ProcessSingleton::DisablePromptForTesting(); + // Put the lock in a temporary directory. Doesn't need to be a + // full profile to test this code. + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + lock_path_ = temp_dir_.path().Append(chrome::kSingletonLockFilename); + socket_path_ = temp_dir_.path().Append(chrome::kSingletonSocketFilename); + cookie_path_ = temp_dir_.path().Append(chrome::kSingletonCookieFilename); + } + + virtual void TearDown() { + io_thread_.Stop(); + testing::Test::TearDown(); + + if (process_singleton_on_thread_) { + worker_thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&ProcessSingletonLinuxTest::DestructProcessSingleton, + base::Unretained(this))); + + scoped_refptr<base::ThreadTestHelper> helper( + new base::ThreadTestHelper( + worker_thread_->message_loop_proxy())); + ASSERT_TRUE(helper->Run()); + } + } + + void CreateProcessSingletonOnThread() { + ASSERT_EQ(NULL, worker_thread_.get()); + worker_thread_.reset(new base::Thread("BlockingThread")); + worker_thread_->Start(); + + worker_thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&ProcessSingletonLinuxTest:: + CreateProcessSingletonInternal, + base::Unretained(this))); + + scoped_refptr<base::ThreadTestHelper> helper( + new base::ThreadTestHelper( + worker_thread_->message_loop_proxy())); + ASSERT_TRUE(helper->Run()); + } + + ProcessSingleton* CreateProcessSingleton() { + return new ProcessSingleton(temp_dir_.path()); + } + + ProcessSingleton::NotifyResult NotifyOtherProcess( + bool override_kill, + int timeout_ms) { + scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); + CommandLine command_line(CommandLine::ForCurrentProcess()->GetProgram()); + command_line.AppendArg("about:blank"); + if (override_kill) { + process_singleton->OverrideCurrentPidForTesting(base::GetCurrentProcId() + 1); + process_singleton->OverrideKillCallbackForTesting( + base::Bind(&ProcessSingletonLinuxTest::KillCallback, + base::Unretained(this))); + } + + return process_singleton->NotifyOtherProcessWithTimeout( + command_line, timeout_ms / 1000, true); + } + + // A helper method to call ProcessSingleton::NotifyOtherProcessOrCreate(). + ProcessSingleton::NotifyResult NotifyOtherProcessOrCreate( + const std::string& url, + int timeout_ms) { + scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); + CommandLine command_line(CommandLine::ForCurrentProcess()->GetProgram()); + command_line.AppendArg(url); + return process_singleton->NotifyOtherProcessWithTimeoutOrCreate( + command_line, base::Bind(&NotificationCallback), timeout_ms / 1000); + } + + void CheckNotified() { + ASSERT_EQ(1u, callback_command_lines_.size()); + bool found = false; + for (size_t i = 0; i < callback_command_lines_[0].size(); ++i) { + if (callback_command_lines_[0][i] == "about:blank") { + found = true; + break; + } + } + ASSERT_TRUE(found); + ASSERT_EQ(0, kill_callbacks_); + } + + void BlockWorkerThread() { + worker_thread_->message_loop()->PostTask( + FROM_HERE, + base::Bind(&ProcessSingletonLinuxTest::BlockThread, + base::Unretained(this))); + } + + void UnblockWorkerThread() { + wait_event_.Signal(); // Unblock the worker thread for shutdown. + signal_event_.Wait(); // Ensure thread unblocks before continuing. + } + + void BlockThread() { + wait_event_.Wait(); + signal_event_.Signal(); + } + + FilePath lock_path_; + FilePath socket_path_; + FilePath cookie_path_; + int kill_callbacks_; + + private: + void CreateProcessSingletonInternal() { + ASSERT_TRUE(!process_singleton_on_thread_); + process_singleton_on_thread_ = CreateProcessSingleton(); + ASSERT_EQ(ProcessSingleton::PROCESS_NONE, + process_singleton_on_thread_->NotifyOtherProcessOrCreate( + base::Bind(&ProcessSingletonLinuxTest::InternalCallback, + base::Unretained(this)))); + } + + void DestructProcessSingleton() { + ASSERT_TRUE(process_singleton_on_thread_); + delete process_singleton_on_thread_; + } + + bool InternalCallback(const CommandLine& command_line, + const FilePath& current_directory) { + callback_command_lines_.push_back(command_line.argv()); + return true; + } + + void KillCallback(int pid) { + kill_callbacks_++; + } + + content::TestBrowserThread io_thread_; + ScopedTempDir temp_dir_; + base::WaitableEvent wait_event_; + base::WaitableEvent signal_event_; + + scoped_ptr<base::Thread> worker_thread_; + ProcessSingleton* process_singleton_on_thread_; + + std::vector<CommandLine::StringVector> callback_command_lines_; +}; + +} // namespace + +// Test if the socket file and symbol link created by ProcessSingletonLinux +// are valid. +// If this test flakes, use http://crbug.com/74554. +TEST_F(ProcessSingletonLinuxTest, CheckSocketFile) { + CreateProcessSingletonOnThread(); + struct stat statbuf; + ASSERT_EQ(0, lstat(lock_path_.value().c_str(), &statbuf)); + ASSERT_TRUE(S_ISLNK(statbuf.st_mode)); + char buf[PATH_MAX]; + ssize_t len = readlink(lock_path_.value().c_str(), buf, PATH_MAX); + ASSERT_GT(len, 0); + + ASSERT_EQ(0, lstat(socket_path_.value().c_str(), &statbuf)); + ASSERT_TRUE(S_ISLNK(statbuf.st_mode)); + + len = readlink(socket_path_.value().c_str(), buf, PATH_MAX); + ASSERT_GT(len, 0); + FilePath socket_target_path = FilePath(std::string(buf, len)); + + ASSERT_EQ(0, lstat(socket_target_path.value().c_str(), &statbuf)); + ASSERT_TRUE(S_ISSOCK(statbuf.st_mode)); + + len = readlink(cookie_path_.value().c_str(), buf, PATH_MAX); + ASSERT_GT(len, 0); + std::string cookie(buf, len); + + FilePath remote_cookie_path = socket_target_path.DirName(). + Append(chrome::kSingletonCookieFilename); + len = readlink(remote_cookie_path.value().c_str(), buf, PATH_MAX); + ASSERT_GT(len, 0); + EXPECT_EQ(cookie, std::string(buf, len)); +} + +// TODO(james.su@gmail.com): port following tests to Windows. +// Test success case of NotifyOtherProcess(). +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessSuccess) { + CreateProcessSingletonOnThread(); + EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, + NotifyOtherProcess(true, TestTimeouts::action_timeout_ms())); + CheckNotified(); +} + +// Test failure case of NotifyOtherProcess(). +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessFailure) { + CreateProcessSingletonOnThread(); + + BlockWorkerThread(); + EXPECT_EQ(ProcessSingleton::PROCESS_NONE, + NotifyOtherProcess(true, TestTimeouts::action_timeout_ms())); + + ASSERT_EQ(1, kill_callbacks_); + UnblockWorkerThread(); +} + +// Test that we don't kill ourselves by accident if a lockfile with the same pid +// happens to exist. +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessNoSuicide) { + CreateProcessSingletonOnThread(); + // Replace lockfile with one containing our own pid. + EXPECT_EQ(0, unlink(lock_path_.value().c_str())); + std::string symlink_content = base::StringPrintf( + "%s%c%u", + net::GetHostName().c_str(), + '-', + base::GetCurrentProcId()); + EXPECT_EQ(0, symlink(symlink_content.c_str(), lock_path_.value().c_str())); + + // Remove socket so that we will not be able to notify the existing browser. + EXPECT_EQ(0, unlink(socket_path_.value().c_str())); + + EXPECT_EQ(ProcessSingleton::PROCESS_NONE, + NotifyOtherProcess(false, TestTimeouts::action_timeout_ms())); + // If we've gotten to this point without killing ourself, the test succeeded. +} + +// Test that we can still notify a process on the same host even after the +// hostname changed. +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessHostChanged) { + CreateProcessSingletonOnThread(); + EXPECT_EQ(0, unlink(lock_path_.value().c_str())); + EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); + + EXPECT_EQ(ProcessSingleton::PROCESS_NOTIFIED, + NotifyOtherProcess(false, TestTimeouts::action_timeout_ms())); + CheckNotified(); +} + +// Test that we fail when lock says process is on another host and we can't +// notify it over the socket. +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessDifferingHost) { + CreateProcessSingletonOnThread(); + + BlockWorkerThread(); + + EXPECT_EQ(0, unlink(lock_path_.value().c_str())); + EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); + + EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, + NotifyOtherProcess(false, TestTimeouts::action_timeout_ms())); + + ASSERT_EQ(0, unlink(lock_path_.value().c_str())); + + UnblockWorkerThread(); +} + +// Test that we fail when lock says process is on another host and we can't +// notify it over the socket. +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessOrCreate_DifferingHost) { + CreateProcessSingletonOnThread(); + + BlockWorkerThread(); + + EXPECT_EQ(0, unlink(lock_path_.value().c_str())); + EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); + + std::string url("about:blank"); + EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, + NotifyOtherProcessOrCreate(url, TestTimeouts::action_timeout_ms())); + + ASSERT_EQ(0, unlink(lock_path_.value().c_str())); + + UnblockWorkerThread(); +} + +// Test that Create fails when another browser is using the profile directory. +TEST_F(ProcessSingletonLinuxTest, CreateFailsWithExistingBrowser) { + CreateProcessSingletonOnThread(); + + scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); + process_singleton->OverrideCurrentPidForTesting(base::GetCurrentProcId() + 1); + EXPECT_FALSE(process_singleton->Create( + base::Bind(&NotificationCallback))); +} + +// Test that Create fails when another browser is using the profile directory +// but with the old socket location. +TEST_F(ProcessSingletonLinuxTest, CreateChecksCompatibilitySocket) { + CreateProcessSingletonOnThread(); + scoped_ptr<ProcessSingleton> process_singleton(CreateProcessSingleton()); + process_singleton->OverrideCurrentPidForTesting(base::GetCurrentProcId() + 1); + + // Do some surgery so as to look like the old configuration. + char buf[PATH_MAX]; + ssize_t len = readlink(socket_path_.value().c_str(), buf, sizeof(buf)); + ASSERT_GT(len, 0); + FilePath socket_target_path = FilePath(std::string(buf, len)); + ASSERT_EQ(0, unlink(socket_path_.value().c_str())); + ASSERT_EQ(0, rename(socket_target_path.value().c_str(), + socket_path_.value().c_str())); + ASSERT_EQ(0, unlink(cookie_path_.value().c_str())); + + EXPECT_FALSE(process_singleton->Create(base::Bind(&NotificationCallback))); +} + +// Test that we fail when lock says process is on another host and we can't +// notify it over the socket before of a bad cookie. +TEST_F(ProcessSingletonLinuxTest, NotifyOtherProcessOrCreate_BadCookie) { + CreateProcessSingletonOnThread(); + // Change the cookie. + EXPECT_EQ(0, unlink(cookie_path_.value().c_str())); + EXPECT_EQ(0, symlink("INCORRECTCOOKIE", cookie_path_.value().c_str())); + + // Also change the hostname, so the remote does not retry. + EXPECT_EQ(0, unlink(lock_path_.value().c_str())); + EXPECT_EQ(0, symlink("FAKEFOOHOST-1234", lock_path_.value().c_str())); + + std::string url("about:blank"); + EXPECT_EQ(ProcessSingleton::PROFILE_IN_USE, + NotifyOtherProcessOrCreate(url, TestTimeouts::action_timeout_ms())); +} + |