| // 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 <windows.h> |
| #include <shellapi.h> |
| #include <stddef.h> |
| |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/debug/activity_tracker.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/process/process.h" |
| #include "base/process/process_info.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/win/registry.h" |
| #include "base/win/scoped_handle.h" |
| #include "base/win/windows_version.h" |
| #include "base/win/wmi.h" |
| #include "chrome/browser/shell_integration.h" |
| #include "chrome/browser/ui/simple_message_box.h" |
| #include "chrome/browser/win/chrome_process_finder.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 "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"; |
| |
| // 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<bool*>(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<wchar_t*>(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<COPYDATASTRUCT*>(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; |
| } |
| |
| bool DisplayShouldKillMessageBox() { |
| return chrome::ShowQuestionMessageBox( |
| NULL, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), |
| l10n_util::GetStringUTF16(IDS_BROWSER_HUNGBROWSER_MESSAGE)) != |
| chrome::MESSAGE_BOX_RESULT_NO; |
| } |
| |
| void SendRemoteProcessInteractionResultHistogram( |
| ProcessSingleton::RemoteProcessInteractionResult result) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Chrome.ProcessSingleton.RemoteProcessInteractionResult", result, |
| ProcessSingleton::REMOTE_PROCESS_INTERACTION_RESULT_COUNT); |
| } |
| |
| // Function was copied from Process::Terminate. |
| void TerminateProcessWithHistograms(const base::Process& process, |
| int exit_code) { |
| DCHECK(process.IsValid()); |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| bool result = (::TerminateProcess(process.Handle(), exit_code) != FALSE); |
| DWORD terminate_error = 0; |
| if (result) { |
| DWORD wait_error = 0; |
| // The process may not end immediately due to pending I/O |
| DWORD wait_result = ::WaitForSingleObject(process.Handle(), 60 * 1000); |
| if (wait_result != WAIT_OBJECT_0) { |
| if (wait_result == WAIT_FAILED) |
| wait_error = ::GetLastError(); |
| SendRemoteProcessInteractionResultHistogram( |
| ProcessSingleton::TERMINATE_WAIT_TIMEOUT); |
| DPLOG(ERROR) << "Error waiting for process exit"; |
| } else { |
| SendRemoteProcessInteractionResultHistogram( |
| ProcessSingleton::TERMINATE_SUCCEEDED); |
| } |
| base::debug::GlobalActivityTracker::RecordProcessExitIfEnabled( |
| process.Pid(), exit_code); |
| UMA_HISTOGRAM_TIMES("Chrome.ProcessSingleton.TerminateProcessTime", |
| base::TimeTicks::Now() - start_time); |
| base::UmaHistogramSparse( |
| "Chrome.ProcessSingleton.TerminationWaitErrorCode.Windows", wait_error); |
| } else { |
| terminate_error = ::GetLastError(); |
| SendRemoteProcessInteractionResultHistogram( |
| ProcessSingleton::TERMINATE_FAILED); |
| DPLOG(ERROR) << "Unable to terminate process"; |
| } |
| base::UmaHistogramSparse( |
| "Chrome.ProcessSingleton.TerminateProcessErrorCode.Windows", |
| terminate_error); |
| } |
| |
| } // 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 (!base::win::WmiLaunchProcess(::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::BindRepeating(&DisplayShouldKillMessageBox)) {} |
| |
| ProcessSingleton::~ProcessSingleton() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 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_)) { |
| case chrome::NOTIFY_SUCCESS: |
| return PROCESS_NOTIFIED; |
| case chrome::NOTIFY_FAILED: |
| remote_window_ = NULL; |
| SendRemoteProcessInteractionResultHistogram(RUNNING_PROCESS_NOTIFY_ERROR); |
| return PROCESS_NONE; |
| case chrome::NOTIFY_WINDOW_HUNG: |
| // Fall through and potentially terminate the hung browser. |
| break; |
| } |
| |
| // The window is hung. |
| DWORD process_id = 0; |
| DWORD thread_id = ::GetWindowThreadProcessId(remote_window_, &process_id); |
| if (!thread_id || !process_id) { |
| remote_window_ = NULL; |
| SendRemoteProcessInteractionResultHistogram(REMOTE_PROCESS_NOT_FOUND); |
| return PROCESS_NONE; |
| } |
| |
| // Get a handle to the process that created the window. |
| base::Process process = base::Process::Open(process_id); |
| |
| // Scan for every window to find a visible one. |
| bool visible_window = false; |
| ::EnumThreadWindows(thread_id, |
| &BrowserWindowEnumeration, |
| reinterpret_cast<LPARAM>(&visible_window)); |
| |
| // If there is a visible browser window, ask the user before killing it. |
| if (visible_window && !should_kill_remote_process_callback_.Run()) { |
| SendRemoteProcessInteractionResultHistogram(USER_REFUSED_TERMINATION); |
| // The user denied. Quit silently. |
| return PROCESS_NOTIFIED; |
| } |
| UMA_HISTOGRAM_ENUMERATION( |
| "Chrome.ProcessSingleton.RemoteHungProcessTerminateReason", |
| visible_window ? USER_ACCEPTED_TERMINATION : NO_VISIBLE_WINDOW_FOUND, |
| REMOTE_HUNG_PROCESS_TERMINATE_REASON_COUNT); |
| |
| // Time to take action. Kill the browser process. |
| TerminateProcessWithHistograms(process, content::RESULT_CODE_HUNG); |
| |
| remote_window_ = NULL; |
| return PROCESS_NONE; |
| } |
| |
| ProcessSingleton::NotifyResult |
| ProcessSingleton::NotifyOtherProcessOrCreate() { |
| const base::TimeTicks begin_ticks = base::TimeTicks::Now(); |
| for (int i = 0; i < 2; ++i) { |
| if (Create()) { |
| UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToCreate", |
| base::TimeTicks::Now() - begin_ticks); |
| return PROCESS_NONE; // This is the single browser process. |
| } |
| ProcessSingleton::NotifyResult result = NotifyOtherProcess(); |
| if (result == PROCESS_NOTIFIED || result == LOCK_ERROR) { |
| if (result == PROCESS_NOTIFIED) { |
| UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToNotify", |
| base::TimeTicks::Now() - begin_ticks); |
| } else { |
| UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToFailure", |
| base::TimeTicks::Now() - begin_ticks); |
| } |
| // The single browser process was notified, the user chose not to |
| // terminate a hung browser, or the lock file could not be created. |
| // Nothing more to do. |
| return result; |
| } |
| DCHECK_EQ(PROCESS_NONE, result); |
| // The process could not be notified for some reason, or it was hung and |
| // terminated. Retry once if this is the first time; otherwise, fall through |
| // to report that the process must exit because the profile is in use. |
| } |
| UMA_HISTOGRAM_MEDIUM_TIMES("Chrome.ProcessSingleton.TimeToFailure", |
| base::TimeTicks::Now() - begin_ticks); |
| return PROFILE_IN_USE; |
| } |
| |
| // 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!"; |
| |
| 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_); |
| |
| 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::BindRepeating(&ProcessLaunchNotification, |
| notification_callback_), |
| user_data_dir_.value()); |
| CHECK(result && window_.hwnd()); |
| } |
| } |
| } |
| |
| return window_.hwnd() != NULL; |
| } |
| |
| void ProcessSingleton::Cleanup() { |
| } |
| |
| void ProcessSingleton::OverrideShouldKillRemoteProcessCallbackForTesting( |
| const ShouldKillRemoteProcessCallback& display_dialog_callback) { |
| should_kill_remote_process_callback_ = display_dialog_callback; |
| } |