Home | History | Annotate | Download | only in browser
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/process_singleton.h"
      6 
      7 #include <shellapi.h>
      8 
      9 #include "base/base_paths.h"
     10 #include "base/bind.h"
     11 #include "base/command_line.h"
     12 #include "base/files/file_path.h"
     13 #include "base/path_service.h"
     14 #include "base/process/kill.h"
     15 #include "base/process/process_info.h"
     16 #include "base/strings/string_number_conversions.h"
     17 #include "base/strings/stringprintf.h"
     18 #include "base/strings/utf_string_conversions.h"
     19 #include "base/time/time.h"
     20 #include "base/win/metro.h"
     21 #include "base/win/registry.h"
     22 #include "base/win/scoped_handle.h"
     23 #include "base/win/win_util.h"
     24 #include "base/win/windows_version.h"
     25 #include "chrome/browser/browser_process.h"
     26 #include "chrome/browser/browser_process_platform_part.h"
     27 #include "chrome/browser/chrome_process_finder_win.h"
     28 #include "chrome/browser/metro_utils/metro_chrome_win.h"
     29 #include "chrome/browser/shell_integration.h"
     30 #include "chrome/browser/ui/simple_message_box.h"
     31 #include "chrome/common/chrome_constants.h"
     32 #include "chrome/common/chrome_paths.h"
     33 #include "chrome/common/chrome_paths_internal.h"
     34 #include "chrome/common/chrome_switches.h"
     35 #include "chrome/installer/util/wmi.h"
     36 #include "content/public/common/result_codes.h"
     37 #include "grit/chromium_strings.h"
     38 #include "grit/generated_resources.h"
     39 #include "net/base/escape.h"
     40 #include "ui/base/l10n/l10n_util.h"
     41 #include "ui/gfx/win/hwnd_util.h"
     42 
     43 namespace {
     44 
     45 const char kLockfile[] = "lockfile";
     46 
     47 const int kMetroChromeActivationTimeoutMs = 3000;
     48 
     49 // A helper class that acquires the given |mutex| while the AutoLockMutex is in
     50 // scope.
     51 class AutoLockMutex {
     52  public:
     53   explicit AutoLockMutex(HANDLE mutex) : mutex_(mutex) {
     54     DWORD result = ::WaitForSingleObject(mutex_, INFINITE);
     55     DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result;
     56   }
     57 
     58   ~AutoLockMutex() {
     59     BOOL released = ::ReleaseMutex(mutex_);
     60     DPCHECK(released);
     61   }
     62 
     63  private:
     64   HANDLE mutex_;
     65   DISALLOW_COPY_AND_ASSIGN(AutoLockMutex);
     66 };
     67 
     68 // A helper class that releases the given |mutex| while the AutoUnlockMutex is
     69 // in scope and immediately re-acquires it when going out of scope.
     70 class AutoUnlockMutex {
     71  public:
     72   explicit AutoUnlockMutex(HANDLE mutex) : mutex_(mutex) {
     73     BOOL released = ::ReleaseMutex(mutex_);
     74     DPCHECK(released);
     75   }
     76 
     77   ~AutoUnlockMutex() {
     78     DWORD result = ::WaitForSingleObject(mutex_, INFINITE);
     79     DPCHECK(result == WAIT_OBJECT_0) << "Result = " << result;
     80   }
     81 
     82  private:
     83   HANDLE mutex_;
     84   DISALLOW_COPY_AND_ASSIGN(AutoUnlockMutex);
     85 };
     86 
     87 // Checks the visibility of the enumerated window and signals once a visible
     88 // window has been found.
     89 BOOL CALLBACK BrowserWindowEnumeration(HWND window, LPARAM param) {
     90   bool* result = reinterpret_cast<bool*>(param);
     91   *result = ::IsWindowVisible(window) != 0;
     92   // Stops enumeration if a visible window has been found.
     93   return !*result;
     94 }
     95 
     96 bool ParseCommandLine(const COPYDATASTRUCT* cds,
     97                       CommandLine* parsed_command_line,
     98                       base::FilePath* current_directory) {
     99   // We should have enough room for the shortest command (min_message_size)
    100   // and also be a multiple of wchar_t bytes. The shortest command
    101   // possible is L"START\0\0" (empty current directory and command line).
    102   static const int min_message_size = 7;
    103   if (cds->cbData < min_message_size * sizeof(wchar_t) ||
    104       cds->cbData % sizeof(wchar_t) != 0) {
    105     LOG(WARNING) << "Invalid WM_COPYDATA, length = " << cds->cbData;
    106     return false;
    107   }
    108 
    109   // We split the string into 4 parts on NULLs.
    110   DCHECK(cds->lpData);
    111   const std::wstring msg(static_cast<wchar_t*>(cds->lpData),
    112                          cds->cbData / sizeof(wchar_t));
    113   const std::wstring::size_type first_null = msg.find_first_of(L'\0');
    114   if (first_null == 0 || first_null == std::wstring::npos) {
    115     // no NULL byte, don't know what to do
    116     LOG(WARNING) << "Invalid WM_COPYDATA, length = " << msg.length() <<
    117       ", first null = " << first_null;
    118     return false;
    119   }
    120 
    121   // Decode the command, which is everything until the first NULL.
    122   if (msg.substr(0, first_null) == L"START") {
    123     // Another instance is starting parse the command line & do what it would
    124     // have done.
    125     VLOG(1) << "Handling STARTUP request from another process";
    126     const std::wstring::size_type second_null =
    127         msg.find_first_of(L'\0', first_null + 1);
    128     if (second_null == std::wstring::npos ||
    129         first_null == msg.length() - 1 || second_null == msg.length()) {
    130       LOG(WARNING) << "Invalid format for start command, we need a string in 4 "
    131         "parts separated by NULLs";
    132       return false;
    133     }
    134 
    135     // Get current directory.
    136     *current_directory = base::FilePath(msg.substr(first_null + 1,
    137                                                    second_null - first_null));
    138 
    139     const std::wstring::size_type third_null =
    140         msg.find_first_of(L'\0', second_null + 1);
    141     if (third_null == std::wstring::npos ||
    142         third_null == msg.length()) {
    143       LOG(WARNING) << "Invalid format for start command, we need a string in 4 "
    144         "parts separated by NULLs";
    145     }
    146 
    147     // Get command line.
    148     const std::wstring cmd_line =
    149         msg.substr(second_null + 1, third_null - second_null);
    150     *parsed_command_line = CommandLine::FromString(cmd_line);
    151     return true;
    152   }
    153   return false;
    154 }
    155 
    156 bool ProcessLaunchNotification(
    157     const ProcessSingleton::NotificationCallback& notification_callback,
    158     UINT message,
    159     WPARAM wparam,
    160     LPARAM lparam,
    161     LRESULT* result) {
    162   if (message != WM_COPYDATA)
    163     return false;
    164 
    165   // Handle the WM_COPYDATA message from another process.
    166   HWND hwnd = reinterpret_cast<HWND>(wparam);
    167   const COPYDATASTRUCT* cds = reinterpret_cast<COPYDATASTRUCT*>(lparam);
    168 
    169   CommandLine parsed_command_line(CommandLine::NO_PROGRAM);
    170   base::FilePath current_directory;
    171   if (!ParseCommandLine(cds, &parsed_command_line, &current_directory)) {
    172     *result = TRUE;
    173     return true;
    174   }
    175 
    176   *result = notification_callback.Run(parsed_command_line, current_directory) ?
    177       TRUE : FALSE;
    178   return true;
    179 }
    180 
    181 // Returns true if Chrome needs to be relaunched into Windows 8 immersive mode.
    182 // Following conditions apply:-
    183 // 1. Windows 8 or greater.
    184 // 2. Not in Windows 8 immersive mode.
    185 // 3. Chrome is default browser.
    186 // 4. Process integrity level is not high.
    187 // 5. The profile data directory is the default directory.
    188 // 6. Last used mode was immersive/machine is a tablet.
    189 // TODO(ananta)
    190 // Move this function to a common place as the Windows 8 delegate_execute
    191 // handler can possibly use this.
    192 bool ShouldLaunchInWindows8ImmersiveMode(const base::FilePath& user_data_dir) {
    193 #if defined(USE_AURA)
    194   // Returning false from this function doesn't mean we don't launch immersive
    195   // mode in Aura. This function is specifically called in case when we need
    196   // to relaunch desktop launched chrome into immersive mode through 'relaunch'
    197   // menu. In case of Aura, we will use delegate_execute to do the relaunch.
    198   return false;
    199 #endif
    200 
    201   if (base::win::GetVersion() < base::win::VERSION_WIN8)
    202     return false;
    203 
    204   if (base::win::IsProcessImmersive(base::GetCurrentProcessHandle()))
    205     return false;
    206 
    207   if (ShellIntegration::GetDefaultBrowser() != ShellIntegration::IS_DEFAULT)
    208     return false;
    209 
    210   base::IntegrityLevel integrity_level = base::INTEGRITY_UNKNOWN;
    211   base::GetProcessIntegrityLevel(base::GetCurrentProcessHandle(),
    212                                  &integrity_level);
    213   if (integrity_level == base::HIGH_INTEGRITY)
    214     return false;
    215 
    216   base::FilePath default_user_data_dir;
    217   if (!chrome::GetDefaultUserDataDirectory(&default_user_data_dir))
    218     return false;
    219 
    220   if (default_user_data_dir != user_data_dir)
    221     return false;
    222 
    223   base::win::RegKey reg_key;
    224   DWORD reg_value = 0;
    225   if (reg_key.Create(HKEY_CURRENT_USER, chrome::kMetroRegistryPath,
    226                      KEY_READ) == ERROR_SUCCESS &&
    227       reg_key.ReadValueDW(chrome::kLaunchModeValue,
    228                           &reg_value) == ERROR_SUCCESS) {
    229     return reg_value == 1;
    230   }
    231   return base::win::IsTouchEnabledDevice();
    232 }
    233 
    234 }  // namespace
    235 
    236 // Microsoft's Softricity virtualization breaks the sandbox processes.
    237 // So, if we detect the Softricity DLL we use WMI Win32_Process.Create to
    238 // break out of the virtualization environment.
    239 // http://code.google.com/p/chromium/issues/detail?id=43650
    240 bool ProcessSingleton::EscapeVirtualization(
    241     const base::FilePath& user_data_dir) {
    242   if (::GetModuleHandle(L"sftldr_wow64.dll") ||
    243       ::GetModuleHandle(L"sftldr.dll")) {
    244     int process_id;
    245     if (!installer::WMIProcess::Launch(::GetCommandLineW(), &process_id))
    246       return false;
    247     is_virtualized_ = true;
    248     // The new window was spawned from WMI, and won't be in the foreground.
    249     // So, first we sleep while the new chrome.exe instance starts (because
    250     // WaitForInputIdle doesn't work here). Then we poll for up to two more
    251     // seconds and make the window foreground if we find it (or we give up).
    252     HWND hwnd = 0;
    253     ::Sleep(90);
    254     for (int tries = 200; tries; --tries) {
    255       hwnd = chrome::FindRunningChromeWindow(user_data_dir);
    256       if (hwnd) {
    257         ::SetForegroundWindow(hwnd);
    258         break;
    259       }
    260       ::Sleep(10);
    261     }
    262     return true;
    263   }
    264   return false;
    265 }
    266 
    267 ProcessSingleton::ProcessSingleton(
    268     const base::FilePath& user_data_dir,
    269     const NotificationCallback& notification_callback)
    270     : notification_callback_(notification_callback),
    271       is_virtualized_(false), lock_file_(INVALID_HANDLE_VALUE),
    272       user_data_dir_(user_data_dir) {
    273 }
    274 
    275 ProcessSingleton::~ProcessSingleton() {
    276   if (lock_file_ != INVALID_HANDLE_VALUE)
    277     ::CloseHandle(lock_file_);
    278 }
    279 
    280 // Code roughly based on Mozilla.
    281 ProcessSingleton::NotifyResult ProcessSingleton::NotifyOtherProcess() {
    282   if (is_virtualized_)
    283     return PROCESS_NOTIFIED;  // We already spawned the process in this case.
    284   if (lock_file_ == INVALID_HANDLE_VALUE && !remote_window_) {
    285     return LOCK_ERROR;
    286   } else if (!remote_window_) {
    287     return PROCESS_NONE;
    288   }
    289 
    290   switch (chrome::AttemptToNotifyRunningChrome(remote_window_, false)) {
    291     case chrome::NOTIFY_SUCCESS:
    292       return PROCESS_NOTIFIED;
    293     case chrome::NOTIFY_FAILED:
    294       remote_window_ = NULL;
    295       return PROCESS_NONE;
    296     case chrome::NOTIFY_WINDOW_HUNG:
    297       remote_window_ = NULL;
    298       break;
    299   }
    300 
    301   DWORD process_id = 0;
    302   DWORD thread_id = ::GetWindowThreadProcessId(remote_window_, &process_id);
    303   if (!thread_id || !process_id) {
    304     remote_window_ = NULL;
    305     return PROCESS_NONE;
    306   }
    307 
    308   // The window is hung. Scan for every window to find a visible one.
    309   bool visible_window = false;
    310   ::EnumThreadWindows(thread_id,
    311                       &BrowserWindowEnumeration,
    312                       reinterpret_cast<LPARAM>(&visible_window));
    313 
    314   // If there is a visible browser window, ask the user before killing it.
    315   if (visible_window &&
    316       chrome::ShowMessageBox(
    317           NULL,
    318           l10n_util::GetStringUTF16(IDS_PRODUCT_NAME),
    319           l10n_util::GetStringUTF16(IDS_BROWSER_HUNGBROWSER_MESSAGE),
    320           chrome::MESSAGE_BOX_TYPE_QUESTION) == chrome::MESSAGE_BOX_RESULT_NO) {
    321     // The user denied. Quit silently.
    322     return PROCESS_NOTIFIED;
    323   }
    324 
    325   // Time to take action. Kill the browser process.
    326   base::KillProcessById(process_id, content::RESULT_CODE_HUNG, true);
    327   remote_window_ = NULL;
    328   return PROCESS_NONE;
    329 }
    330 
    331 ProcessSingleton::NotifyResult
    332 ProcessSingleton::NotifyOtherProcessOrCreate() {
    333   ProcessSingleton::NotifyResult result = PROCESS_NONE;
    334   if (!Create()) {
    335     result = NotifyOtherProcess();
    336     if (result == PROCESS_NONE)
    337       result = PROFILE_IN_USE;
    338   } else {
    339     g_browser_process->platform_part()->PlatformSpecificCommandLineProcessing(
    340         *CommandLine::ForCurrentProcess());
    341   }
    342   return result;
    343 }
    344 
    345 // Look for a Chrome instance that uses the same profile directory. If there
    346 // isn't one, create a message window with its title set to the profile
    347 // directory path.
    348 bool ProcessSingleton::Create() {
    349   static const wchar_t kMutexName[] = L"Local\\ChromeProcessSingletonStartup!";
    350   static const wchar_t kMetroActivationEventName[] =
    351       L"Local\\ChromeProcessSingletonStartupMetroActivation!";
    352 
    353   remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_);
    354   if (!remote_window_ && !EscapeVirtualization(user_data_dir_)) {
    355     // Make sure we will be the one and only process creating the window.
    356     // We use a named Mutex since we are protecting against multi-process
    357     // access. As documented, it's clearer to NOT request ownership on creation
    358     // since it isn't guaranteed we will get it. It is better to create it
    359     // without ownership and explicitly get the ownership afterward.
    360     base::win::ScopedHandle only_me(::CreateMutex(NULL, FALSE, kMutexName));
    361     DPCHECK(only_me.IsValid());
    362 
    363     AutoLockMutex auto_lock_only_me(only_me);
    364 
    365     // We now own the mutex so we are the only process that can create the
    366     // window at this time, but we must still check if someone created it
    367     // between the time where we looked for it above and the time the mutex
    368     // was given to us.
    369     remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_);
    370 
    371 
    372     // In Win8+, a new Chrome process launched in Desktop mode may need to be
    373     // transmuted into Metro Chrome (see ShouldLaunchInWindows8ImmersiveMode for
    374     // heuristics). To accomplish this, the current Chrome activates Metro
    375     // Chrome, releases the startup mutex, and waits for metro Chrome to take
    376     // the singleton. From that point onward, the command line for this Chrome
    377     // process will be sent to Metro Chrome by the usual channels.
    378     if (!remote_window_ && base::win::GetVersion() >= base::win::VERSION_WIN8 &&
    379         !base::win::IsMetroProcess()) {
    380       // |metro_activation_event| is created right before activating a Metro
    381       // Chrome (note that there can only be one Metro Chrome process; by OS
    382       // design); all following Desktop processes will then wait for this event
    383       // to be signaled by Metro Chrome which will do so as soon as it grabs
    384       // this singleton (should any of the waiting processes timeout waiting for
    385       // the signal they will try to grab the singleton for themselves which
    386       // will result in a forced Desktop Chrome launch in the worst case).
    387       base::win::ScopedHandle metro_activation_event(
    388           ::OpenEvent(SYNCHRONIZE, FALSE, kMetroActivationEventName));
    389       if (!metro_activation_event.IsValid() &&
    390           ShouldLaunchInWindows8ImmersiveMode(user_data_dir_)) {
    391         // No Metro activation is under way, but the desire is to launch in
    392         // Metro mode: activate and rendez-vous with the activated process.
    393         metro_activation_event.Set(
    394             ::CreateEvent(NULL, TRUE, FALSE, kMetroActivationEventName));
    395         if (!chrome::ActivateMetroChrome()) {
    396           // Failed to launch immersive Chrome, default to launching on Desktop.
    397           LOG(ERROR) << "Failed to launch immersive chrome";
    398           metro_activation_event.Close();
    399         }
    400       }
    401 
    402       if (metro_activation_event.IsValid()) {
    403         // Release |only_me| (to let Metro Chrome grab this singleton) and wait
    404         // until the event is signaled (i.e. Metro Chrome was successfully
    405         // activated). Ignore timeout waiting for |metro_activation_event|.
    406         {
    407           AutoUnlockMutex auto_unlock_only_me(only_me);
    408 
    409           DWORD result = ::WaitForSingleObject(metro_activation_event,
    410                                                kMetroChromeActivationTimeoutMs);
    411           DPCHECK(result == WAIT_OBJECT_0 || result == WAIT_TIMEOUT)
    412               << "Result = " << result;
    413         }
    414 
    415         // Check if this singleton was successfully grabbed by another process
    416         // (hopefully Metro Chrome). Failing to do so, this process will grab
    417         // the singleton and launch in Desktop mode.
    418         remote_window_ = chrome::FindRunningChromeWindow(user_data_dir_);
    419       }
    420     }
    421 
    422     if (!remote_window_) {
    423       // We have to make sure there is no Chrome instance running on another
    424       // machine that uses the same profile.
    425       base::FilePath lock_file_path = user_data_dir_.AppendASCII(kLockfile);
    426       lock_file_ = ::CreateFile(lock_file_path.value().c_str(),
    427                                 GENERIC_WRITE,
    428                                 FILE_SHARE_READ,
    429                                 NULL,
    430                                 CREATE_ALWAYS,
    431                                 FILE_ATTRIBUTE_NORMAL |
    432                                 FILE_FLAG_DELETE_ON_CLOSE,
    433                                 NULL);
    434       DWORD error = ::GetLastError();
    435       LOG_IF(WARNING, lock_file_ != INVALID_HANDLE_VALUE &&
    436           error == ERROR_ALREADY_EXISTS) << "Lock file exists but is writable.";
    437       LOG_IF(ERROR, lock_file_ == INVALID_HANDLE_VALUE)
    438           << "Lock file can not be created! Error code: " << error;
    439 
    440       if (lock_file_ != INVALID_HANDLE_VALUE) {
    441         // Set the window's title to the path of our user data directory so
    442         // other Chrome instances can decide if they should forward to us.
    443         bool result = window_.CreateNamed(
    444             base::Bind(&ProcessLaunchNotification, notification_callback_),
    445             user_data_dir_.value());
    446         CHECK(result && window_.hwnd());
    447       }
    448 
    449       if (base::win::GetVersion() >= base::win::VERSION_WIN8) {
    450         // Make sure no one is still waiting on Metro activation whether it
    451         // succeeded (i.e., this is the Metro process) or failed.
    452         base::win::ScopedHandle metro_activation_event(
    453             ::OpenEvent(EVENT_MODIFY_STATE, FALSE, kMetroActivationEventName));
    454         if (metro_activation_event.IsValid())
    455           ::SetEvent(metro_activation_event);
    456       }
    457     }
    458   }
    459 
    460   return window_.hwnd() != NULL;
    461 }
    462 
    463 void ProcessSingleton::Cleanup() {
    464 }
    465