diff --git a/atom/browser/process_singleton.h b/atom/browser/process_singleton.h new file mode 100644 index 000000000000..3260c4ef240c --- /dev/null +++ b/atom/browser/process_singleton.h @@ -0,0 +1,183 @@ +// 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. + +#ifndef CHROME_BROWSER_PROCESS_SINGLETON_H_ +#define CHROME_BROWSER_PROCESS_SINGLETON_H_ + +#include "build/build_config.h" + +#if defined(OS_WIN) +#include +#endif // defined(OS_WIN) + +#include +#include + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/process/process.h" +#include "base/threading/non_thread_safe.h" +#include "ui/gfx/native_widget_types.h" + +#if defined(OS_POSIX) && !defined(OS_ANDROID) +#include "base/files/scoped_temp_dir.h" +#endif + +#if defined(OS_WIN) +#include "base/win/message_window.h" +#endif // defined(OS_WIN) + +namespace base { +class CommandLine; +} + +// ProcessSingleton ---------------------------------------------------------- +// +// This class allows different browser processes to communicate with +// each other. It is named according to the user data directory, so +// we can be sure that no more than one copy of the application can be +// running at once with a given data directory. +// +// Implementation notes: +// - the Windows implementation uses an invisible global message window; +// - the Linux implementation uses a Unix domain socket in the user data dir. + +class ProcessSingleton : public base::NonThreadSafe { + public: + enum NotifyResult { + PROCESS_NONE, + PROCESS_NOTIFIED, + PROFILE_IN_USE, + LOCK_ERROR, + }; + + // Implement this callback to handle notifications from other processes. The + // callback will receive the command line and directory with which the other + // Chrome process was launched. Return true if the command line will be + // handled within the current browser instance or false if the remote process + // should handle it (i.e., because the current process is shutting down). + using NotificationCallback = + base::Callback; + + ProcessSingleton(const base::FilePath& user_data_dir, + const NotificationCallback& notification_callback); + ~ProcessSingleton(); + + // Notify another process, if available. Otherwise sets ourselves as the + // singleton instance. Returns PROCESS_NONE if we became the singleton + // instance. Callers are guaranteed to either have notified an existing + // process or have grabbed the singleton (unless the profile is locked by an + // unreachable process). + // TODO(brettw): Make the implementation of this method non-platform-specific + // by making Linux re-use the Windows implementation. + NotifyResult NotifyOtherProcessOrCreate(); + + // Sets ourself up as the singleton instance. Returns true on success. If + // false is returned, we are not the singleton instance and the caller must + // exit. + // NOTE: Most callers should generally prefer NotifyOtherProcessOrCreate() to + // this method, only callers for whom failure is preferred to notifying + // another process should call this directly. + bool Create(); + + // Clear any lock state during shutdown. + void Cleanup(); + +#if defined(OS_POSIX) && !defined(OS_ANDROID) + static void DisablePromptForTesting(); +#endif +#if defined(OS_WIN) + // Called to query whether to kill a hung browser process that has visible + // windows. Return true to allow killing the hung process. + using ShouldKillRemoteProcessCallback = base::Callback; + void OverrideShouldKillRemoteProcessCallbackForTesting( + const ShouldKillRemoteProcessCallback& display_dialog_callback); +#endif + + protected: + // Notify another process, if available. + // Returns true if another process was found and notified, false if we should + // continue with the current process. + // On Windows, Create() has to be called before this. + NotifyResult NotifyOtherProcess(); + +#if defined(OS_POSIX) && !defined(OS_ANDROID) + // Exposed for testing. We use a timeout on Linux, and in tests we want + // this timeout to be short. + NotifyResult NotifyOtherProcessWithTimeout( + const base::CommandLine& command_line, + int retry_attempts, + const base::TimeDelta& timeout, + bool kill_unresponsive); + NotifyResult NotifyOtherProcessWithTimeoutOrCreate( + const base::CommandLine& command_line, + int retry_attempts, + const base::TimeDelta& timeout); + void OverrideCurrentPidForTesting(base::ProcessId pid); + void OverrideKillCallbackForTesting( + const base::Callback& callback); +#endif + + private: + NotificationCallback notification_callback_; // Handler for notifications. + +#if defined(OS_WIN) + bool EscapeVirtualization(const base::FilePath& user_data_dir); + + HWND remote_window_; // The HWND_MESSAGE of another browser. + base::win::MessageWindow window_; // The message-only window. + bool is_virtualized_; // Stuck inside Microsoft Softricity VM environment. + HANDLE lock_file_; + base::FilePath user_data_dir_; + ShouldKillRemoteProcessCallback should_kill_remote_process_callback_; +#elif defined(OS_POSIX) && !defined(OS_ANDROID) + // 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 kill_callback_; + + // Path in file system to the socket. + base::FilePath socket_path_; + + // Path in file system to the lock. + base::FilePath lock_path_; + + // Path in file system to the cookie file. + base::FilePath cookie_path_; + + // Temporary directory to hold the socket. + base::ScopedTempDir socket_dir_; + + // Helper class for linux specific messages. LinuxWatcher is ref counted + // because it posts messages between threads. + class LinuxWatcher; + scoped_refptr watcher_; +#endif + + DISALLOW_COPY_AND_ASSIGN(ProcessSingleton); +}; + +#endif // CHROME_BROWSER_PROCESS_SINGLETON_H_ diff --git a/atom/browser/process_singleton_posix.cc b/atom/browser/process_singleton_posix.cc new file mode 100644 index 000000000000..81a5c5fec355 --- /dev/null +++ b/atom/browser/process_singleton_posix.cc @@ -0,0 +1,1062 @@ +// Copyright 2014 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. + +// On Linux, when the user tries to launch a second copy of chrome, we check +// for a socket in the user's profile directory. If the socket file is open we +// send a message to the first chrome browser process with the current +// directory and second process command line flags. The second process then +// exits. +// +// Because many networked filesystem implementations do not support unix domain +// sockets, we create the socket in a temporary directory and create a symlink +// in the profile. This temporary directory is no longer bound to the profile, +// and may disappear across a reboot or login to a separate session. To bind +// them, we store a unique cookie in the profile directory, which must also be +// present in the remote directory to connect. The cookie is checked both before +// and after the connection. /tmp is sticky, and different Chrome sessions use +// different cookies. Thus, a matching cookie before and after means the +// connection was to a directory with a valid cookie. +// +// We also have a lock file, which is a symlink to a non-existent destination. +// The destination is a string containing the hostname and process id of +// chrome's browser process, eg. "SingletonLock -> example.com-9156". When the +// first copy of chrome exits it will delete the lock file on shutdown, so that +// a different instance on a different host may then use the profile directory. +// +// If writing to the socket fails, the hostname in the lock is checked to see if +// another instance is running a different host using a shared filesystem (nfs, +// etc.) If the hostname differs an error is displayed and the second process +// exits. Otherwise the first process (if any) is killed and the second process +// starts as normal. +// +// When the second process sends the current directory and command line flags to +// the first process, it waits for an ACK message back from the first process +// for a certain time. If there is no ACK message back in time, then the first +// 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. + +#include "chrome/browser/process_singleton.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "base/base_paths.h" +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/posix/eintr_wrapper.h" +#include "base/posix/safe_strerror.h" +#include "base/rand_util.h" +#include "base/sequenced_task_runner_helpers.h" +#include "base/single_thread_task_runner.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/platform_thread.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/grit/chromium_strings.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/browser_thread.h" +#include "net/base/net_util.h" +#include "ui/base/l10n/l10n_util.h" + +#if defined(OS_LINUX) +#include "chrome/browser/ui/process_singleton_dialog_linux.h" +#endif + +#if defined(TOOLKIT_VIEWS) && defined(OS_LINUX) && !defined(OS_CHROMEOS) +#include "ui/views/linux_ui/linux_ui.h" +#endif + +using content::BrowserThread; + +namespace { + +// Timeout for the current browser process to respond. 20 seconds should be +// enough. +const int kTimeoutInSeconds = 20; +// Number of retries to notify the browser. 20 retries over 20 seconds = 1 try +// per second. +const int kRetryAttempts = 20; +static bool g_disable_prompt; +const char kStartToken[] = "START"; +const char kACKToken[] = "ACK"; +const char kShutdownToken[] = "SHUTDOWN"; +const char kTokenDelimiter = '\0'; +const int kMaxMessageLength = 32 * 1024; +const int kMaxACKMessageLength = arraysize(kShutdownToken) - 1; + +const char kLockDelimiter = '-'; + +// Set the close-on-exec bit on a file descriptor. +// Returns 0 on success, -1 on failure. +int SetCloseOnExec(int fd) { + int flags = fcntl(fd, F_GETFD, 0); + if (-1 == flags) + return flags; + if (flags & FD_CLOEXEC) + return 0; + return fcntl(fd, F_SETFD, flags | FD_CLOEXEC); +} + +// Close a socket and check return value. +void CloseSocket(int fd) { + int rv = IGNORE_EINTR(close(fd)); + DCHECK_EQ(0, rv) << "Error closing socket: " << base::safe_strerror(errno); +} + +// Write a message to a socket fd. +bool WriteToSocket(int fd, const char *message, size_t length) { + DCHECK(message); + DCHECK(length); + size_t bytes_written = 0; + do { + ssize_t rv = HANDLE_EINTR( + write(fd, message + bytes_written, length - bytes_written)); + if (rv < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // The socket shouldn't block, we're sending so little data. Just give + // up here, since NotifyOtherProcess() doesn't have an asynchronous api. + LOG(ERROR) << "ProcessSingleton would block on write(), so it gave up."; + return false; + } + PLOG(ERROR) << "write() failed"; + return false; + } + bytes_written += rv; + } while (bytes_written < length); + + return true; +} + +struct timeval TimeDeltaToTimeVal(const base::TimeDelta& delta) { + struct timeval result; + result.tv_sec = delta.InSeconds(); + result.tv_usec = delta.InMicroseconds() % base::Time::kMicrosecondsPerSecond; + return result; +} + +// Wait a socket for read for a certain timeout. +// Returns -1 if error occurred, 0 if timeout reached, > 0 if the socket is +// ready for read. +int WaitSocketForRead(int fd, const base::TimeDelta& timeout) { + fd_set read_fds; + struct timeval tv = TimeDeltaToTimeVal(timeout); + + FD_ZERO(&read_fds); + FD_SET(fd, &read_fds); + + return HANDLE_EINTR(select(fd + 1, &read_fds, NULL, NULL, &tv)); +} + +// Read a message from a socket fd, with an optional timeout. +// If |timeout| <= 0 then read immediately. +// Return number of bytes actually read, or -1 on error. +ssize_t ReadFromSocket(int fd, + char* buf, + size_t bufsize, + const base::TimeDelta& timeout) { + if (timeout > base::TimeDelta()) { + int rv = WaitSocketForRead(fd, timeout); + if (rv <= 0) + return rv; + } + + size_t bytes_read = 0; + do { + ssize_t rv = HANDLE_EINTR(read(fd, buf + bytes_read, bufsize - bytes_read)); + if (rv < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + PLOG(ERROR) << "read() failed"; + return rv; + } else { + // It would block, so we just return what has been read. + return bytes_read; + } + } else if (!rv) { + // No more data to read. + return bytes_read; + } else { + bytes_read += rv; + } + } while (bytes_read < bufsize); + + return bytes_read; +} + +// Set up a sockaddr appropriate for messaging. +void SetupSockAddr(const std::string& path, struct sockaddr_un* addr) { + addr->sun_family = AF_UNIX; + CHECK(path.length() < arraysize(addr->sun_path)) + << "Socket path too long: " << path; + base::strlcpy(addr->sun_path, path.c_str(), arraysize(addr->sun_path)); +} + +// Set up a socket appropriate for messaging. +int SetupSocketOnly() { + int sock = socket(PF_UNIX, SOCK_STREAM, 0); + PCHECK(sock >= 0) << "socket() failed"; + + int rv = net::SetNonBlocking(sock); + DCHECK_EQ(0, rv) << "Failed to make non-blocking socket."; + rv = SetCloseOnExec(sock); + DCHECK_EQ(0, rv) << "Failed to set CLOEXEC on socket."; + + return sock; +} + +// Set up a socket and sockaddr appropriate for messaging. +void SetupSocket(const std::string& path, int* sock, struct sockaddr_un* addr) { + *sock = SetupSocketOnly(); + SetupSockAddr(path, addr); +} + +// Read a symbolic link, return empty string if given path is not a symbol link. +base::FilePath ReadLink(const base::FilePath& path) { + base::FilePath target; + if (!base::ReadSymbolicLink(path, &target)) { + // The only errno that should occur is ENOENT. + if (errno != 0 && errno != ENOENT) + PLOG(ERROR) << "readlink(" << path.value() << ") failed"; + } + return target; +} + +// Unlink a path. Return true on success. +bool UnlinkPath(const base::FilePath& path) { + int rv = unlink(path.value().c_str()); + if (rv < 0 && errno != ENOENT) + PLOG(ERROR) << "Failed to unlink " << path.value(); + + return rv == 0; +} + +// Create a symlink. Returns true on success. +bool SymlinkPath(const base::FilePath& target, const base::FilePath& path) { + if (!base::CreateSymbolicLink(target, path)) { + // Double check the value in case symlink suceeded but we got an incorrect + // failure due to NFS packet loss & retry. + int saved_errno = errno; + if (ReadLink(path) != target) { + // If we failed to create the lock, most likely another instance won the + // startup race. + errno = saved_errno; + PLOG(ERROR) << "Failed to create " << path.value(); + return false; + } + } + return true; +} + +// Extract the hostname and pid from the lock symlink. +// Returns true if the lock existed. +bool ParseLockPath(const base::FilePath& path, + std::string* hostname, + int* pid) { + std::string real_path = ReadLink(path).value(); + if (real_path.empty()) + return false; + + std::string::size_type pos = real_path.rfind(kLockDelimiter); + + // If the path is not a symbolic link, or doesn't contain what we expect, + // bail. + if (pos == std::string::npos) { + *hostname = ""; + *pid = -1; + return true; + } + + *hostname = real_path.substr(0, pos); + + const std::string& pid_str = real_path.substr(pos + 1); + if (!base::StringToInt(pid_str, pid)) + *pid = -1; + + return true; +} + +// Returns true if the user opted to unlock the profile. +bool DisplayProfileInUseError(const base::FilePath& lock_path, + const std::string& hostname, + int pid) { + base::string16 error = l10n_util::GetStringFUTF16( + IDS_PROFILE_IN_USE_POSIX, + base::IntToString16(pid), + base::ASCIIToUTF16(hostname)); + LOG(ERROR) << error; + + if (g_disable_prompt) + return false; + +#if defined(OS_LINUX) + base::string16 relaunch_button_text = l10n_util::GetStringUTF16( + IDS_PROFILE_IN_USE_LINUX_RELAUNCH); + return ShowProcessSingletonDialog(error, relaunch_button_text); +#elif defined(OS_MACOSX) + // On Mac, always usurp the lock. + return true; +#endif + + NOTREACHED(); + return false; +} + +bool IsChromeProcess(pid_t pid) { + base::FilePath other_chrome_path(base::GetProcessExecutablePath(pid)); + return (!other_chrome_path.empty() && + other_chrome_path.BaseName() == + base::FilePath(chrome::kBrowserProcessExecutableName)); +} + +// A helper class to hold onto a socket. +class ScopedSocket { + public: + ScopedSocket() : fd_(-1) { Reset(); } + ~ScopedSocket() { Close(); } + int fd() { return fd_; } + void Reset() { + Close(); + fd_ = SetupSocketOnly(); + } + void Close() { + if (fd_ >= 0) + CloseSocket(fd_); + fd_ = -1; + } + private: + int fd_; +}; + +// Returns a random string for uniquifying profile connections. +std::string GenerateCookie() { + return base::Uint64ToString(base::RandUint64()); +} + +bool CheckCookie(const base::FilePath& path, const base::FilePath& cookie) { + return (cookie == ReadLink(path)); +} + +bool ConnectSocket(ScopedSocket* socket, + const base::FilePath& socket_path, + const base::FilePath& cookie_path) { + base::FilePath socket_target; + if (base::ReadSymbolicLink(socket_path, &socket_target)) { + // It's a symlink. Read the cookie. + base::FilePath cookie = ReadLink(cookie_path); + if (cookie.empty()) + return false; + base::FilePath remote_cookie = socket_target.DirName(). + Append(chrome::kSingletonCookieFilename); + // Verify the cookie before connecting. + if (!CheckCookie(remote_cookie, cookie)) + return false; + // Now we know the directory was (at that point) created by the profile + // owner. Try to connect. + sockaddr_un addr; + SetupSockAddr(socket_target.value(), &addr); + int ret = HANDLE_EINTR(connect(socket->fd(), + reinterpret_cast(&addr), + sizeof(addr))); + if (ret != 0) + return false; + // Check the cookie again. We only link in /tmp, which is sticky, so, if the + // directory is still correct, it must have been correct in-between when we + // connected. POSIX, sadly, lacks a connectat(). + if (!CheckCookie(remote_cookie, cookie)) { + socket->Reset(); + return false; + } + // Success! + return true; + } else if (errno == EINVAL) { + // It exists, but is not a symlink (or some other error we detect + // later). Just connect to it directly; this is an older version of Chrome. + sockaddr_un addr; + SetupSockAddr(socket_path.value(), &addr); + int ret = HANDLE_EINTR(connect(socket->fd(), + reinterpret_cast(&addr), + sizeof(addr))); + return (ret == 0); + } else { + // File is missing, or other error. + if (errno != ENOENT) + PLOG(ERROR) << "readlink failed"; + return false; + } +} + +#if defined(OS_MACOSX) +bool ReplaceOldSingletonLock(const base::FilePath& symlink_content, + const base::FilePath& lock_path) { + // Try taking an flock(2) on the file. Failure means the lock is taken so we + // should quit. + base::ScopedFD lock_fd(HANDLE_EINTR( + open(lock_path.value().c_str(), O_RDWR | O_CREAT | O_SYMLINK, 0644))); + if (!lock_fd.is_valid()) { + PLOG(ERROR) << "Could not open singleton lock"; + return false; + } + + int rc = HANDLE_EINTR(flock(lock_fd.get(), LOCK_EX | LOCK_NB)); + if (rc == -1) { + if (errno == EWOULDBLOCK) { + LOG(ERROR) << "Singleton lock held by old process."; + } else { + PLOG(ERROR) << "Error locking singleton lock"; + } + return false; + } + + // Successfully taking the lock means we can replace it with the a new symlink + // lock. We never flock() the lock file from now on. I.e. we assume that an + // old version of Chrome will not run with the same user data dir after this + // version has run. + if (!base::DeleteFile(lock_path, false)) { + PLOG(ERROR) << "Could not delete old singleton lock."; + return false; + } + + return SymlinkPath(symlink_content, lock_path); +} +#endif // defined(OS_MACOSX) + +} // namespace + +/////////////////////////////////////////////////////////////////////////////// +// ProcessSingleton::LinuxWatcher +// A helper class for a Linux specific implementation of the process singleton. +// This class sets up a listener on the singleton socket and handles parsing +// messages that come in on the singleton socket. +class ProcessSingleton::LinuxWatcher + : public base::MessageLoopForIO::Watcher, + public base::MessageLoop::DestructionObserver, + public base::RefCountedThreadSafe { + public: + // A helper class to read message from an established socket. + class SocketReader : public base::MessageLoopForIO::Watcher { + public: + SocketReader(ProcessSingleton::LinuxWatcher* parent, + base::MessageLoop* ui_message_loop, + int fd) + : parent_(parent), + ui_message_loop_(ui_message_loop), + fd_(fd), + bytes_read_(0) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + // Wait for reads. + base::MessageLoopForIO::current()->WatchFileDescriptor( + fd, true, base::MessageLoopForIO::WATCH_READ, &fd_reader_, this); + // If we haven't completed in a reasonable amount of time, give up. + timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(kTimeoutInSeconds), + this, &SocketReader::CleanupAndDeleteSelf); + } + + ~SocketReader() override { CloseSocket(fd_); } + + // MessageLoopForIO::Watcher impl. + void OnFileCanReadWithoutBlocking(int fd) override; + void OnFileCanWriteWithoutBlocking(int fd) override { + // SocketReader only watches for accept (read) events. + NOTREACHED(); + } + + // Finish handling the incoming message by optionally sending back an ACK + // message and removing this SocketReader. + void FinishWithACK(const char *message, size_t length); + + private: + void CleanupAndDeleteSelf() { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + + parent_->RemoveSocketReader(this); + // We're deleted beyond this point. + } + + base::MessageLoopForIO::FileDescriptorWatcher fd_reader_; + + // The ProcessSingleton::LinuxWatcher that owns us. + ProcessSingleton::LinuxWatcher* const parent_; + + // A reference to the UI message loop. + base::MessageLoop* const ui_message_loop_; + + // The file descriptor we're reading. + const int fd_; + + // Store the message in this buffer. + char buf_[kMaxMessageLength]; + + // Tracks the number of bytes we've read in case we're getting partial + // reads. + size_t bytes_read_; + + base::OneShotTimer timer_; + + DISALLOW_COPY_AND_ASSIGN(SocketReader); + }; + + // We expect to only be constructed on the UI thread. + explicit LinuxWatcher(ProcessSingleton* parent) + : ui_message_loop_(base::MessageLoop::current()), + parent_(parent) { + } + + // Start listening for connections on the socket. This method should be + // called from the IO thread. + void StartListening(int socket); + + // This method determines if we should use the same process and if we should, + // opens a new browser tab. This runs on the UI thread. + // |reader| is for sending back ACK message. + void HandleMessage(const std::string& current_dir, + const std::vector& argv, + SocketReader* reader); + + // MessageLoopForIO::Watcher impl. These run on the IO thread. + void OnFileCanReadWithoutBlocking(int fd) override; + void OnFileCanWriteWithoutBlocking(int fd) override { + // ProcessSingleton only watches for accept (read) events. + NOTREACHED(); + } + + // MessageLoop::DestructionObserver + void WillDestroyCurrentMessageLoop() override { + fd_watcher_.StopWatchingFileDescriptor(); + } + + private: + friend struct BrowserThread::DeleteOnThread; + friend class base::DeleteHelper; + + ~LinuxWatcher() override { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + STLDeleteElements(&readers_); + + base::MessageLoopForIO* ml = base::MessageLoopForIO::current(); + ml->RemoveDestructionObserver(this); + } + + // Removes and deletes the SocketReader. + void RemoveSocketReader(SocketReader* reader); + + base::MessageLoopForIO::FileDescriptorWatcher fd_watcher_; + + // A reference to the UI message loop (i.e., the message loop we were + // constructed on). + base::MessageLoop* ui_message_loop_; + + // The ProcessSingleton that owns us. + ProcessSingleton* const parent_; + + std::set readers_; + + DISALLOW_COPY_AND_ASSIGN(LinuxWatcher); +}; + +void ProcessSingleton::LinuxWatcher::OnFileCanReadWithoutBlocking(int fd) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + // Accepting incoming client. + sockaddr_un from; + socklen_t from_len = sizeof(from); + int connection_socket = HANDLE_EINTR(accept( + fd, reinterpret_cast(&from), &from_len)); + if (-1 == connection_socket) { + PLOG(ERROR) << "accept() failed"; + return; + } + int rv = net::SetNonBlocking(connection_socket); + DCHECK_EQ(0, rv) << "Failed to make non-blocking socket."; + SocketReader* reader = new SocketReader(this, + ui_message_loop_, + connection_socket); + readers_.insert(reader); +} + +void ProcessSingleton::LinuxWatcher::StartListening(int socket) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + // Watch for client connections on this socket. + base::MessageLoopForIO* ml = base::MessageLoopForIO::current(); + ml->AddDestructionObserver(this); + ml->WatchFileDescriptor(socket, true, base::MessageLoopForIO::WATCH_READ, + &fd_watcher_, this); +} + +void ProcessSingleton::LinuxWatcher::HandleMessage( + const std::string& current_dir, const std::vector& argv, + SocketReader* reader) { + DCHECK(ui_message_loop_ == base::MessageLoop::current()); + DCHECK(reader); + + if (parent_->notification_callback_.Run(base::CommandLine(argv), + base::FilePath(current_dir))) { + // Send back "ACK" message to prevent the client process from starting up. + reader->FinishWithACK(kACKToken, arraysize(kACKToken) - 1); + } else { + LOG(WARNING) << "Not handling interprocess notification as browser" + " is shutting down"; + // Send back "SHUTDOWN" message, so that the client process can start up + // without killing this process. + reader->FinishWithACK(kShutdownToken, arraysize(kShutdownToken) - 1); + return; + } +} + +void ProcessSingleton::LinuxWatcher::RemoveSocketReader(SocketReader* reader) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + DCHECK(reader); + readers_.erase(reader); + delete reader; +} + +/////////////////////////////////////////////////////////////////////////////// +// ProcessSingleton::LinuxWatcher::SocketReader +// + +void ProcessSingleton::LinuxWatcher::SocketReader::OnFileCanReadWithoutBlocking( + int fd) { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + DCHECK_EQ(fd, fd_); + while (bytes_read_ < sizeof(buf_)) { + ssize_t rv = HANDLE_EINTR( + read(fd, buf_ + bytes_read_, sizeof(buf_) - bytes_read_)); + if (rv < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + PLOG(ERROR) << "read() failed"; + CloseSocket(fd); + return; + } else { + // It would block, so we just return and continue to watch for the next + // opportunity to read. + return; + } + } else if (!rv) { + // No more data to read. It's time to process the message. + break; + } else { + bytes_read_ += rv; + } + } + + // Validate the message. The shortest message is kStartToken\0x\0x + const size_t kMinMessageLength = arraysize(kStartToken) + 4; + if (bytes_read_ < kMinMessageLength) { + buf_[bytes_read_] = 0; + LOG(ERROR) << "Invalid socket message (wrong length):" << buf_; + CleanupAndDeleteSelf(); + return; + } + + std::string str(buf_, bytes_read_); + std::vector tokens = base::SplitString( + str, std::string(1, kTokenDelimiter), + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + + if (tokens.size() < 3 || tokens[0] != kStartToken) { + LOG(ERROR) << "Wrong message format: " << str; + CleanupAndDeleteSelf(); + return; + } + + // Stop the expiration timer to prevent this SocketReader object from being + // terminated unexpectly. + timer_.Stop(); + + std::string current_dir = tokens[1]; + // Remove the first two tokens. The remaining tokens should be the command + // line argv array. + tokens.erase(tokens.begin()); + tokens.erase(tokens.begin()); + + // Return to the UI thread to handle opening a new browser tab. + ui_message_loop_->task_runner()->PostTask( + FROM_HERE, base::Bind(&ProcessSingleton::LinuxWatcher::HandleMessage, + parent_, current_dir, tokens, this)); + fd_reader_.StopWatchingFileDescriptor(); + + // LinuxWatcher::HandleMessage() is in charge of destroying this SocketReader + // object by invoking SocketReader::FinishWithACK(). +} + +void ProcessSingleton::LinuxWatcher::SocketReader::FinishWithACK( + const char *message, size_t length) { + if (message && length) { + // Not necessary to care about the return value. + WriteToSocket(fd_, message, length); + } + + if (shutdown(fd_, SHUT_WR) < 0) + PLOG(ERROR) << "shutdown() failed"; + + BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind(&ProcessSingleton::LinuxWatcher::RemoveSocketReader, + parent_, + this)); + // We will be deleted once the posted RemoveSocketReader task runs. +} + +/////////////////////////////////////////////////////////////////////////////// +// ProcessSingleton +// +ProcessSingleton::ProcessSingleton( + const base::FilePath& user_data_dir, + const NotificationCallback& notification_callback) + : notification_callback_(notification_callback), + current_pid_(base::GetCurrentProcId()), + 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() { +} + +ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() { + return NotifyOtherProcessWithTimeout( + *base::CommandLine::ForCurrentProcess(), kRetryAttempts, + base::TimeDelta::FromSeconds(kTimeoutInSeconds), true); +} + +ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessWithTimeout( + const base::CommandLine& cmd_line, + int retry_attempts, + const base::TimeDelta& timeout, + bool kill_unresponsive) { + DCHECK_GE(retry_attempts, 0); + DCHECK_GE(timeout.InMicroseconds(), 0); + + base::TimeDelta sleep_interval = timeout / retry_attempts; + + ScopedSocket socket; + for (int retries = 0; retries <= retry_attempts; ++retries) { + // Try to connect to the socket. + if (ConnectSocket(&socket, socket_path_, cookie_path_)) + break; + + // If we're in a race with another process, they may be in Create() and have + // created the lock but not attached to the socket. So we check if the + // process with the pid from the lockfile is currently running and is a + // chrome browser. If so, we loop and try again for |timeout|. + + std::string hostname; + int pid; + if (!ParseLockPath(lock_path_, &hostname, &pid)) { + // No lockfile exists. + return PROCESS_NONE; + } + + if (hostname.empty()) { + // Invalid lockfile. + UnlinkPath(lock_path_); + return PROCESS_NONE; + } + + if (hostname != net::GetHostName() && !IsChromeProcess(pid)) { + // Locked by process on another host. If the user selected to unlock + // the profile, try to continue; otherwise quit. + if (DisplayProfileInUseError(lock_path_, hostname, pid)) { + UnlinkPath(lock_path_); + return PROCESS_NONE; + } + return PROFILE_IN_USE; + } + + if (!IsChromeProcess(pid)) { + // Orphaned lockfile (no process with pid, or non-chrome process.) + UnlinkPath(lock_path_); + return PROCESS_NONE; + } + + if (IsSameChromeInstance(pid)) { + // Orphaned lockfile (pid is part of same chrome instance we are, even + // though we haven't tried to create a lockfile yet). + UnlinkPath(lock_path_); + return PROCESS_NONE; + } + + if (retries == retry_attempts) { + // Retries failed. Kill the unresponsive chrome process and continue. + if (!kill_unresponsive || !KillProcessByLockPath()) + return PROFILE_IN_USE; + return PROCESS_NONE; + } + + base::PlatformThread::Sleep(sleep_interval); + } + + timeval socket_timeout = TimeDeltaToTimeVal(timeout); + setsockopt(socket.fd(), + SOL_SOCKET, + SO_SNDTIMEO, + &socket_timeout, + sizeof(socket_timeout)); + + // Found another process, prepare our command line + // format is "START\0\0\0...\0". + std::string to_send(kStartToken); + to_send.push_back(kTokenDelimiter); + + base::FilePath current_dir; + if (!PathService::Get(base::DIR_CURRENT, ¤t_dir)) + return PROCESS_NONE; + to_send.append(current_dir.value()); + + const std::vector& argv = cmd_line.argv(); + for (std::vector::const_iterator it = argv.begin(); + it != argv.end(); ++it) { + to_send.push_back(kTokenDelimiter); + to_send.append(*it); + } + + // 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()) + return PROFILE_IN_USE; + return PROCESS_NONE; + } + + if (shutdown(socket.fd(), SHUT_WR) < 0) + PLOG(ERROR) << "shutdown() failed"; + + // Read ACK message from the other process. It might be blocked for a certain + // timeout, to make sure the other process has enough time to return ACK. + char buf[kMaxACKMessageLength + 1]; + ssize_t len = ReadFromSocket(socket.fd(), buf, kMaxACKMessageLength, timeout); + + // Failed to read ACK, the other process might have been frozen. + if (len <= 0) { + if (!kill_unresponsive || !KillProcessByLockPath()) + return PROFILE_IN_USE; + return PROCESS_NONE; + } + + buf[len] = '\0'; + if (strncmp(buf, kShutdownToken, arraysize(kShutdownToken) - 1) == 0) { + // The other process is shutting down, it's safe to start a new process. + return PROCESS_NONE; + } else if (strncmp(buf, kACKToken, arraysize(kACKToken) - 1) == 0) { +#if defined(TOOLKIT_VIEWS) && defined(OS_LINUX) && !defined(OS_CHROMEOS) + // Likely NULL in unit tests. + views::LinuxUI* linux_ui = views::LinuxUI::instance(); + if (linux_ui) + linux_ui->NotifyWindowManagerStartupComplete(); +#endif + + // Assume the other process is handling the request. + return PROCESS_NOTIFIED; + } + + NOTREACHED() << "The other process returned unknown message: " << buf; + return PROCESS_NOTIFIED; +} + +ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcessOrCreate() { + return NotifyOtherProcessWithTimeoutOrCreate( + *base::CommandLine::ForCurrentProcess(), kRetryAttempts, + base::TimeDelta::FromSeconds(kTimeoutInSeconds)); +} + +ProcessSingleton::NotifyResult +ProcessSingleton::NotifyOtherProcessWithTimeoutOrCreate( + const base::CommandLine& command_line, + int retry_attempts, + const base::TimeDelta& timeout) { + NotifyResult result = NotifyOtherProcessWithTimeout( + command_line, retry_attempts, timeout, true); + if (result != PROCESS_NONE) + return result; + if (Create()) + return PROCESS_NONE; + // If the Create() failed, try again to notify. (It could be that another + // instance was starting at the same time and managed to grab the lock before + // we did.) + // This time, we don't want to kill anything if we aren't successful, since we + // aren't going to try to take over the lock ourselves. + result = NotifyOtherProcessWithTimeout( + command_line, retry_attempts, timeout, false); + if (result != PROCESS_NONE) + return result; + + return LOCK_ERROR; +} + +void ProcessSingleton::OverrideCurrentPidForTesting(base::ProcessId pid) { + current_pid_ = pid; +} + +void ProcessSingleton::OverrideKillCallbackForTesting( + const base::Callback& callback) { + kill_callback_ = callback; +} + +void ProcessSingleton::DisablePromptForTesting() { + g_disable_prompt = true; +} + +bool ProcessSingleton::Create() { + int sock; + sockaddr_un addr; + + // The symlink lock is pointed to the hostname and process id, so other + // processes can find it out. + base::FilePath symlink_content(base::StringPrintf( + "%s%c%u", + net::GetHostName().c_str(), + kLockDelimiter, + current_pid_)); + + // Create symbol link before binding the socket, to ensure only one instance + // can have the socket open. + if (!SymlinkPath(symlink_content, lock_path_)) { + // TODO(jackhou): Remove this case once this code is stable on Mac. + // http://crbug.com/367612 +#if defined(OS_MACOSX) + // On Mac, an existing non-symlink lock file means the lock could be held by + // the old process singleton code. If we can successfully replace the lock, + // continue as normal. + if (base::IsLink(lock_path_) || + !ReplaceOldSingletonLock(symlink_content, lock_path_)) { + return false; + } +#else + // If we failed to create the lock, most likely another instance won the + // startup race. + return false; +#endif + } + + // Create the socket file somewhere in /tmp which is usually mounted as a + // normal filesystem. Some network filesystems (notably AFS) are screwy and + // do not support Unix domain sockets. + if (!socket_dir_.CreateUniqueTempDir()) { + LOG(ERROR) << "Failed to create socket directory."; + return false; + } + + // Check that the directory was created with the correct permissions. + int dir_mode = 0; + CHECK(base::GetPosixFilePermissions(socket_dir_.path(), &dir_mode) && + dir_mode == base::FILE_PERMISSION_USER_MASK) + << "Temp directory mode is not 700: " << std::oct << dir_mode; + + // Setup the socket symlink and the two cookies. + base::FilePath socket_target_path = + socket_dir_.path().Append(chrome::kSingletonSocketFilename); + base::FilePath cookie(GenerateCookie()); + base::FilePath remote_cookie_path = + socket_dir_.path().Append(chrome::kSingletonCookieFilename); + UnlinkPath(socket_path_); + UnlinkPath(cookie_path_); + if (!SymlinkPath(socket_target_path, socket_path_) || + !SymlinkPath(cookie, cookie_path_) || + !SymlinkPath(cookie, remote_cookie_path)) { + // We've already locked things, so we can't have lost the startup race, + // but something doesn't like us. + LOG(ERROR) << "Failed to create symlinks."; + if (!socket_dir_.Delete()) + LOG(ERROR) << "Encountered a problem when deleting socket directory."; + return false; + } + + SetupSocket(socket_target_path.value(), &sock, &addr); + + if (bind(sock, reinterpret_cast(&addr), sizeof(addr)) < 0) { + PLOG(ERROR) << "Failed to bind() " << socket_target_path.value(); + CloseSocket(sock); + return false; + } + + if (listen(sock, 5) < 0) + NOTREACHED() << "listen failed: " << base::safe_strerror(errno); + + DCHECK(BrowserThread::IsMessageLoopValid(BrowserThread::IO)); + BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind(&ProcessSingleton::LinuxWatcher::StartListening, + watcher_.get(), + sock)); + + return true; +} + +void ProcessSingleton::Cleanup() { + UnlinkPath(socket_path_); + 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()) { + return DisplayProfileInUseError(lock_path_, hostname, pid); + } + 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(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: " + << base::safe_strerror(errno); +} diff --git a/atom/browser/process_singleton_startup_lock.cc b/atom/browser/process_singleton_startup_lock.cc new file mode 100644 index 000000000000..f564de7dca4d --- /dev/null +++ b/atom/browser/process_singleton_startup_lock.cc @@ -0,0 +1,53 @@ +// Copyright (c) 2013 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_startup_lock.h" + +#include "base/bind.h" +#include "base/logging.h" + +ProcessSingletonStartupLock::ProcessSingletonStartupLock( + const ProcessSingleton::NotificationCallback& original_callback) + : locked_(true), + original_callback_(original_callback) {} + +ProcessSingletonStartupLock::~ProcessSingletonStartupLock() {} + +ProcessSingleton::NotificationCallback +ProcessSingletonStartupLock::AsNotificationCallback() { + return base::Bind(&ProcessSingletonStartupLock::NotificationCallbackImpl, + base::Unretained(this)); +} + +void ProcessSingletonStartupLock::Unlock() { + DCHECK(CalledOnValidThread()); + locked_ = false; + + // Replay the command lines of the messages which were received while the + // ProcessSingleton was locked. Only replay each message once. + std::set replayed_messages; + for (std::vector::const_iterator it = + saved_startup_messages_.begin(); + it != saved_startup_messages_.end(); ++it) { + if (replayed_messages.find(*it) != replayed_messages.end()) + continue; + original_callback_.Run(base::CommandLine(it->first), it->second); + replayed_messages.insert(*it); + } + saved_startup_messages_.clear(); +} + +bool ProcessSingletonStartupLock::NotificationCallbackImpl( + const base::CommandLine& command_line, + const base::FilePath& current_directory) { + if (locked_) { + // If locked, it means we are not ready to process this message because + // we are probably in a first run critical phase. + saved_startup_messages_.push_back( + std::make_pair(command_line.argv(), current_directory)); + return true; + } else { + return original_callback_.Run(command_line, current_directory); + } +} diff --git a/atom/browser/process_singleton_startup_lock.h b/atom/browser/process_singleton_startup_lock.h new file mode 100644 index 000000000000..187dd35bce3d --- /dev/null +++ b/atom/browser/process_singleton_startup_lock.h @@ -0,0 +1,57 @@ +// Copyright (c) 2013 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_BROWSER_PROCESS_SINGLETON_STARTUP_LOCK_H_ +#define CHROME_BROWSER_PROCESS_SINGLETON_STARTUP_LOCK_H_ + +#include +#include +#include + +#include "base/basictypes.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/threading/non_thread_safe.h" +#include "chrome/browser/process_singleton.h" + +// Provides a ProcessSingleton::NotificationCallback that can queue up +// command-line invocations during startup and execute them when startup +// completes. +// +// The object starts in a locked state. |Unlock()| must be called +// when the process is prepared to handle command-line invocations. +// +// Once unlocked, notifications are forwarded to a wrapped NotificationCallback. +class ProcessSingletonStartupLock : public base::NonThreadSafe { + public: + explicit ProcessSingletonStartupLock( + const ProcessSingleton::NotificationCallback& original_callback); + ~ProcessSingletonStartupLock(); + + // Returns the ProcessSingleton::NotificationCallback. + // The callback is only valid during the lifetime of the + // ProcessSingletonStartupLock instance. + ProcessSingleton::NotificationCallback AsNotificationCallback(); + + // Executes previously queued command-line invocations and allows future + // invocations to be executed immediately. + void Unlock(); + + bool locked() { return locked_; } + + private: + typedef std::pair + DelayedStartupMessage; + + bool NotificationCallbackImpl(const base::CommandLine& command_line, + const base::FilePath& current_directory); + + bool locked_; + std::vector saved_startup_messages_; + ProcessSingleton::NotificationCallback original_callback_; + + DISALLOW_COPY_AND_ASSIGN(ProcessSingletonStartupLock); +}; + +#endif // CHROME_BROWSER_PROCESS_SINGLETON_STARTUP_LOCK_H_ diff --git a/atom/browser/process_singleton_win.cc b/atom/browser/process_singleton_win.cc new file mode 100644 index 000000000000..623aa7f42f7f --- /dev/null +++ b/atom/browser/process_singleton_win.cc @@ -0,0 +1,441 @@ +// 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 + +#include "base/base_paths.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/process/process.h" +#include "base/process/process_info.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/win/metro.h" +#include "base/win/registry.h" +#include "base/win/scoped_handle.h" +#include "base/win/windows_version.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browser_process_platform_part.h" +#include "chrome/browser/chrome_process_finder_win.h" +#include "chrome/browser/metro_utils/metro_chrome_win.h" +#include "chrome/browser/shell_integration.h" +#include "chrome/browser/ui/simple_message_box.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/chrome_paths_internal.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/grit/chromium_strings.h" +#include "chrome/installer/util/wmi.h" +#include "content/public/common/result_codes.h" +#include "net/base/escape.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/win/hwnd_util.h" + +namespace { + +const char kLockfile[] = "lockfile"; + +const int kMetroChromeActivationTimeoutMs = 3000; + +// A helper class that acquires the given |mutex| while the AutoLockMutex is in +// scope. +class AutoLockMutex { + public: + explicit AutoLockMutex(HANDLE mutex) : mutex_(mutex) { + DWORD result = ::WaitForSingleObject(mutex_, INFINITE); + DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result; + } + + ~AutoLockMutex() { + BOOL released = ::ReleaseMutex(mutex_); + DPCHECK(released); + } + + private: + HANDLE mutex_; + DISALLOW_COPY_AND_ASSIGN(AutoLockMutex); +}; + +// A helper class that releases the given |mutex| while the AutoUnlockMutex is +// in scope and immediately re-acquires it when going out of scope. +class AutoUnlockMutex { + public: + explicit AutoUnlockMutex(HANDLE mutex) : mutex_(mutex) { + BOOL released = ::ReleaseMutex(mutex_); + DPCHECK(released); + } + + ~AutoUnlockMutex() { + DWORD result = ::WaitForSingleObject(mutex_, INFINITE); + DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result; + } + + private: + HANDLE mutex_; + DISALLOW_COPY_AND_ASSIGN(AutoUnlockMutex); +}; + +// Checks the visibility of the enumerated window and signals once a visible +// window has been found. +BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) { + bool* result = reinterpret_cast(param); + *result = ::IsWindowVisible(window) != 0; + // Stops enumeration if a visible window has been found. + return !*result; +} + +bool ParseCommandLine(const COPYDATASTRUCT* cds, + base::CommandLine* parsed_command_line, + base::FilePath* current_directory) { + // We should have enough room for the shortest command (min_message_size) + // and also be a multiple of wchar_t bytes. The shortest command + // possible is L"START\0\0" (empty current directory and command line). + static const int min_message_size = 7; + if (cds->cbData < min_message_size * sizeof(wchar_t) || + cds->cbData % sizeof(wchar_t) != 0) { + LOG(WARNING) << "Invalid WM_COPYDATA, length = " << cds->cbData; + return false; + } + + // We split the string into 4 parts on NULLs. + DCHECK(cds->lpData); + const std::wstring msg(static_cast(cds->lpData), + cds->cbData / sizeof(wchar_t)); + const std::wstring::size_type first_null = msg.find_first_of(L'\0'); + if (first_null == 0 || first_null == std::wstring::npos) { + // no NULL byte, don't know what to do + LOG(WARNING) << "Invalid WM_COPYDATA, length = " << msg.length() << + ", first null = " << first_null; + return false; + } + + // Decode the command, which is everything until the first NULL. + if (msg.substr(0, first_null) == L"START") { + // Another instance is starting parse the command line & do what it would + // have done. + VLOG(1) << "Handling STARTUP request from another process"; + const std::wstring::size_type second_null = + msg.find_first_of(L'\0', first_null + 1); + if (second_null == std::wstring::npos || + first_null == msg.length() - 1 || second_null == msg.length()) { + LOG(WARNING) << "Invalid format for start command, we need a string in 4 " + "parts separated by NULLs"; + return false; + } + + // Get current directory. + *current_directory = base::FilePath(msg.substr(first_null + 1, + second_null - first_null)); + + const std::wstring::size_type third_null = + msg.find_first_of(L'\0', second_null + 1); + if (third_null == std::wstring::npos || + third_null == msg.length()) { + LOG(WARNING) << "Invalid format for start command, we need a string in 4 " + "parts separated by NULLs"; + } + + // Get command line. + const std::wstring cmd_line = + msg.substr(second_null + 1, third_null - second_null); + *parsed_command_line = base::CommandLine::FromString(cmd_line); + return true; + } + return false; +} + +bool ProcessLaunchNotification( + const ProcessSingleton::NotificationCallback& notification_callback, + UINT message, + WPARAM wparam, + LPARAM lparam, + LRESULT* result) { + if (message != WM_COPYDATA) + return false; + + // Handle the WM_COPYDATA message from another process. + const COPYDATASTRUCT* cds = reinterpret_cast(lparam); + + base::CommandLine parsed_command_line(base::CommandLine::NO_PROGRAM); + base::FilePath current_directory; + if (!ParseCommandLine(cds, &parsed_command_line, ¤t_directory)) { + *result = TRUE; + return true; + } + + *result = notification_callback.Run(parsed_command_line, current_directory) ? + TRUE : FALSE; + return true; +} + +// Returns true if Chrome needs to be relaunched into Windows 8 immersive mode. +// Following conditions apply:- +// 1. Windows 8 or greater. +// 2. Not in Windows 8 immersive mode. +// 3. Chrome is default browser. +// 4. Process integrity level is not high. +// 5. The profile data directory is the default directory. +// 6. Last used mode was immersive/machine is a tablet. +// TODO(ananta) +// Move this function to a common place as the Windows 8 delegate_execute +// handler can possibly use this. +bool ShouldLaunchInWindows8ImmersiveMode(const base::FilePath& user_data_dir) { + // Returning false from this function doesn't mean we don't launch immersive + // mode in Aura. This function is specifically called in case when we need + // to relaunch desktop launched chrome into immersive mode through 'relaunch' + // menu. In case of Aura, we will use delegate_execute to do the relaunch. + return false; +} + +bool DisplayShouldKillMessageBox() { + return chrome::ShowMessageBox( + NULL, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), + l10n_util::GetStringUTF16(IDS_BROWSER_HUNGBROWSER_MESSAGE), + chrome::MESSAGE_BOX_TYPE_QUESTION) != + chrome::MESSAGE_BOX_RESULT_NO; +} + +} // namespace + +// Microsoft's Softricity virtualization breaks the sandbox processes. +// So, if we detect the Softricity DLL we use WMI Win32_Process.Create to +// break out of the virtualization environment. +// http://code.google.com/p/chromium/issues/detail?id=43650 +bool ProcessSingleton::EscapeVirtualization( + const base::FilePath& user_data_dir) { + if (::GetModuleHandle(L"sftldr_wow64.dll") || + ::GetModuleHandle(L"sftldr.dll")) { + int process_id; + if (!installer::WMIProcess::Launch(::GetCommandLineW(), &process_id)) + return false; + is_virtualized_ = true; + // The new window was spawned from WMI, and won't be in the foreground. + // So, first we sleep while the new chrome.exe instance starts (because + // WaitForInputIdle doesn't work here). Then we poll for up to two more + // seconds and make the window foreground if we find it (or we give up). + HWND hwnd = 0; + ::Sleep(90); + for (int tries = 200; tries; --tries) { + hwnd = chrome::FindRunningChromeWindow(user_data_dir); + if (hwnd) { + ::SetForegroundWindow(hwnd); + break; + } + ::Sleep(10); + } + return true; + } + return false; +} + +ProcessSingleton::ProcessSingleton( + const base::FilePath& user_data_dir, + const NotificationCallback& notification_callback) + : notification_callback_(notification_callback), + is_virtualized_(false), + lock_file_(INVALID_HANDLE_VALUE), + user_data_dir_(user_data_dir), + should_kill_remote_process_callback_( + base::Bind(&DisplayShouldKillMessageBox)) { +} + +ProcessSingleton::~ProcessSingleton() { + if (lock_file_ != INVALID_HANDLE_VALUE) + ::CloseHandle(lock_file_); +} + +// Code roughly based on Mozilla. +ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() { + if (is_virtualized_) + return PROCESS_NOTIFIED; // We already spawned the process in this case. + if (lock_file_ == INVALID_HANDLE_VALUE && !remote_window_) { + return LOCK_ERROR; + } else if (!remote_window_) { + return PROCESS_NONE; + } + + switch (chrome::AttemptToNotifyRunningChrome(remote_window_, false)) { + case chrome::NOTIFY_SUCCESS: + return PROCESS_NOTIFIED; + case chrome::NOTIFY_FAILED: + remote_window_ = NULL; + return PROCESS_NONE; + case chrome::NOTIFY_WINDOW_HUNG: + // Fall through and potentially terminate the hung browser. + break; + } + + DWORD process_id = 0; + DWORD thread_id = ::GetWindowThreadProcessId(remote_window_, &process_id); + if (!thread_id || !process_id) { + remote_window_ = NULL; + return PROCESS_NONE; + } + base::Process process = base::Process::Open(process_id); + + // The window is hung. Scan for every window to find a visible one. + bool visible_window = false; + ::EnumThreadWindows(thread_id, + &BrowserWindowEnumeration, + reinterpret_cast(&visible_window)); + + // If there is a visible browser window, ask the user before killing it. + if (visible_window && !should_kill_remote_process_callback_.Run()) { + // The user denied. Quit silently. + return PROCESS_NOTIFIED; + } + + // Time to take action. Kill the browser process. + process.Terminate(content::RESULT_CODE_HUNG, true); + remote_window_ = NULL; + return PROCESS_NONE; +} + +ProcessSingleton::NotifyResult +ProcessSingleton::NotifyOtherProcessOrCreate() { + ProcessSingleton::NotifyResult result = PROCESS_NONE; + if (!Create()) { + result = NotifyOtherProcess(); + if (result == PROCESS_NONE) + result = PROFILE_IN_USE; + } else { + g_browser_process->platform_part()->PlatformSpecificCommandLineProcessing( + *base::CommandLine::ForCurrentProcess()); + } + return result; +} + +// Look for a Chrome instance that uses the same profile directory. If there +// isn't one, create a message window with its title set to the profile +// directory path. +bool ProcessSingleton::Create() { + static const wchar_t kMutexName[] = L"Local\\ChromeProcessSingletonStartup!"; + static const wchar_t kMetroActivationEventName[] = + L"Local\\ChromeProcessSingletonStartupMetroActivation!"; + + remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_); + if (!remote_window_ && !EscapeVirtualization(user_data_dir_)) { + // Make sure we will be the one and only process creating the window. + // We use a named Mutex since we are protecting against multi-process + // access. As documented, it's clearer to NOT request ownership on creation + // since it isn't guaranteed we will get it. It is better to create it + // without ownership and explicitly get the ownership afterward. + base::win::ScopedHandle only_me(::CreateMutex(NULL, FALSE, kMutexName)); + if (!only_me.IsValid()) { + DPLOG(FATAL) << "CreateMutex failed"; + return false; + } + + AutoLockMutex auto_lock_only_me(only_me.Get()); + + // We now own the mutex so we are the only process that can create the + // window at this time, but we must still check if someone created it + // between the time where we looked for it above and the time the mutex + // was given to us. + remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_); + + + // In Win8+, a new Chrome process launched in Desktop mode may need to be + // transmuted into Metro Chrome (see ShouldLaunchInWindows8ImmersiveMode for + // heuristics). To accomplish this, the current Chrome activates Metro + // Chrome, releases the startup mutex, and waits for metro Chrome to take + // the singleton. From that point onward, the command line for this Chrome + // process will be sent to Metro Chrome by the usual channels. + if (!remote_window_ && base::win::GetVersion() >= base::win::VERSION_WIN8 && + !base::win::IsMetroProcess()) { + // |metro_activation_event| is created right before activating a Metro + // Chrome (note that there can only be one Metro Chrome process; by OS + // design); all following Desktop processes will then wait for this event + // to be signaled by Metro Chrome which will do so as soon as it grabs + // this singleton (should any of the waiting processes timeout waiting for + // the signal they will try to grab the singleton for themselves which + // will result in a forced Desktop Chrome launch in the worst case). + base::win::ScopedHandle metro_activation_event( + ::OpenEvent(SYNCHRONIZE, FALSE, kMetroActivationEventName)); + if (!metro_activation_event.IsValid() && + ShouldLaunchInWindows8ImmersiveMode(user_data_dir_)) { + // No Metro activation is under way, but the desire is to launch in + // Metro mode: activate and rendez-vous with the activated process. + metro_activation_event.Set( + ::CreateEvent(NULL, TRUE, FALSE, kMetroActivationEventName)); + if (!chrome::ActivateMetroChrome()) { + // Failed to launch immersive Chrome, default to launching on Desktop. + LOG(ERROR) << "Failed to launch immersive chrome"; + metro_activation_event.Close(); + } + } + + if (metro_activation_event.IsValid()) { + // Release |only_me| (to let Metro Chrome grab this singleton) and wait + // until the event is signaled (i.e. Metro Chrome was successfully + // activated). Ignore timeout waiting for |metro_activation_event|. + { + AutoUnlockMutex auto_unlock_only_me(only_me.Get()); + + DWORD result = ::WaitForSingleObject(metro_activation_event.Get(), + kMetroChromeActivationTimeoutMs); + DPCHECK(result == WAIT_OBJECT_0 || result == WAIT_TIMEOUT) + << "Result = " << result; + } + + // Check if this singleton was successfully grabbed by another process + // (hopefully Metro Chrome). Failing to do so, this process will grab + // the singleton and launch in Desktop mode. + remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_); + } + } + + if (!remote_window_) { + // We have to make sure there is no Chrome instance running on another + // machine that uses the same profile. + base::FilePath lock_file_path = user_data_dir_.AppendASCII(kLockfile); + lock_file_ = ::CreateFile(lock_file_path.value().c_str(), + GENERIC_WRITE, + FILE_SHARE_READ, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL | + FILE_FLAG_DELETE_ON_CLOSE, + NULL); + DWORD error = ::GetLastError(); + LOG_IF(WARNING, lock_file_ != INVALID_HANDLE_VALUE && + error == ERROR_ALREADY_EXISTS) << "Lock file exists but is writable."; + LOG_IF(ERROR, lock_file_ == INVALID_HANDLE_VALUE) + << "Lock file can not be created! Error code: " << error; + + if (lock_file_ != INVALID_HANDLE_VALUE) { + // Set the window's title to the path of our user data directory so + // other Chrome instances can decide if they should forward to us. + bool result = window_.CreateNamed( + base::Bind(&ProcessLaunchNotification, notification_callback_), + user_data_dir_.value()); + CHECK(result && window_.hwnd()); + } + + if (base::win::GetVersion() >= base::win::VERSION_WIN8) { + // Make sure no one is still waiting on Metro activation whether it + // succeeded (i.e., this is the Metro process) or failed. + base::win::ScopedHandle metro_activation_event( + ::OpenEvent(EVENT_MODIFY_STATE, FALSE, kMetroActivationEventName)); + if (metro_activation_event.IsValid()) + ::SetEvent(metro_activation_event.Get()); + } + } + } + + return window_.hwnd() != NULL; +} + +void ProcessSingleton::Cleanup() { +} + +void ProcessSingleton::OverrideShouldKillRemoteProcessCallbackForTesting( + const ShouldKillRemoteProcessCallback& display_dialog_callback) { + should_kill_remote_process_callback_ = display_dialog_callback; +}