普通文本  |  686行  |  25.54 KB

// 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/test/chromedriver/chrome_launcher.h"

#include <algorithm>
#include <vector>

#include "base/base64.h"
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/format_macros.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/process/kill.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/test/chromedriver/chrome/chrome_android_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_existing_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_finder.h"
#include "chrome/test/chromedriver/chrome/device_manager.h"
#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
#include "chrome/test/chromedriver/chrome/embedded_automation_extension.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/user_data_dir.h"
#include "chrome/test/chromedriver/chrome/version.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/chrome/zip.h"
#include "chrome/test/chromedriver/net/port_server.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h"
#include "crypto/sha2.h"

#if defined(OS_POSIX)
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#endif

namespace {

const char* kCommonSwitches[] = {
    "ignore-certificate-errors", "metrics-recording-only"};

#if defined(OS_LINUX)
const char* kEnableCrashReport = "enable-crash-reporter-for-testing";
#endif

Status UnpackAutomationExtension(const base::FilePath& temp_dir,
                                 base::FilePath* automation_extension) {
  std::string decoded_extension;
  if (!base::Base64Decode(kAutomationExtension, &decoded_extension))
    return Status(kUnknownError, "failed to base64decode automation extension");

  base::FilePath extension_zip = temp_dir.AppendASCII("internal.zip");
  int size = static_cast<int>(decoded_extension.length());
  if (file_util::WriteFile(extension_zip, decoded_extension.c_str(), size)
      != size) {
    return Status(kUnknownError, "failed to write automation extension zip");
  }

  base::FilePath extension_dir = temp_dir.AppendASCII("internal");
  if (!zip::Unzip(extension_zip, extension_dir))
    return Status(kUnknownError, "failed to unzip automation extension");

  *automation_extension = extension_dir;
  return Status(kOk);
}

Status PrepareCommandLine(int port,
                          const Capabilities& capabilities,
                          CommandLine* prepared_command,
                          base::ScopedTempDir* user_data_dir,
                          base::ScopedTempDir* extension_dir,
                          std::vector<std::string>* extension_bg_pages) {
  base::FilePath program = capabilities.binary;
  if (program.empty()) {
    if (!FindChrome(&program))
      return Status(kUnknownError, "cannot find Chrome binary");
  } else if (!base::PathExists(program)) {
    return Status(kUnknownError,
                  base::StringPrintf("no chrome binary at %" PRFilePath,
                                     program.value().c_str()));
  }
  CommandLine command(program);
  Switches switches;

  // TODO(chrisgao): Add "disable-sync" when chrome 30- is not supported.
  // For chrome 30-, it leads to crash when opening chrome://settings.
  for (size_t i = 0; i < arraysize(kCommonSwitches); ++i)
    switches.SetSwitch(kCommonSwitches[i]);
  switches.SetSwitch("disable-hang-monitor");
  switches.SetSwitch("disable-prompt-on-repost");
  switches.SetSwitch("full-memory-crash-report");
  switches.SetSwitch("no-first-run");
  switches.SetSwitch("disable-background-networking");
  switches.SetSwitch("disable-web-resources");
  switches.SetSwitch("safebrowsing-disable-auto-update");
  switches.SetSwitch("safebrowsing-disable-download-protection");
  switches.SetSwitch("disable-client-side-phishing-detection");
  switches.SetSwitch("disable-component-update");
  switches.SetSwitch("disable-default-apps");
  switches.SetSwitch("enable-logging");
  switches.SetSwitch("logging-level", "1");
  switches.SetSwitch("password-store", "basic");
  switches.SetSwitch("use-mock-keychain");
  switches.SetSwitch("remote-debugging-port", base::IntToString(port));

  for (std::set<std::string>::const_iterator iter =
           capabilities.exclude_switches.begin();
       iter != capabilities.exclude_switches.end();
       ++iter) {
    switches.RemoveSwitch(*iter);
  }
  switches.SetFromSwitches(capabilities.switches);

  if (!switches.HasSwitch("user-data-dir")) {
    command.AppendArg("data:,");
    if (!user_data_dir->CreateUniqueTempDir())
      return Status(kUnknownError, "cannot create temp dir for user data dir");
    switches.SetSwitch("user-data-dir", user_data_dir->path().value());
    Status status = internal::PrepareUserDataDir(
        user_data_dir->path(), capabilities.prefs.get(),
        capabilities.local_state.get());
    if (status.IsError())
      return status;
  }

  if (!extension_dir->CreateUniqueTempDir()) {
    return Status(kUnknownError,
                  "cannot create temp dir for unpacking extensions");
  }
  Status status = internal::ProcessExtensions(capabilities.extensions,
                                              extension_dir->path(),
                                              true,
                                              &switches,
                                              extension_bg_pages);
  if (status.IsError())
    return status;
  switches.AppendToCommandLine(&command);
  *prepared_command = command;
  return Status(kOk);
}

Status WaitForDevToolsAndCheckVersion(
    const NetAddress& address,
    URLRequestContextGetter* context_getter,
    const SyncWebSocketFactory& socket_factory,
    scoped_ptr<DevToolsHttpClient>* user_client) {
  scoped_ptr<DevToolsHttpClient> client(new DevToolsHttpClient(
      address, context_getter, socket_factory));
  base::TimeTicks deadline =
      base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
  Status status = client->Init(deadline - base::TimeTicks::Now());
  if (status.IsError())
    return status;
  if (client->build_no() < kMinimumSupportedChromeBuildNo) {
    return Status(kUnknownError, "Chrome version must be >= " +
        GetMinimumSupportedChromeVersion());
  }

  while (base::TimeTicks::Now() < deadline) {
    WebViewsInfo views_info;
    client->GetWebViewsInfo(&views_info);
    for (size_t i = 0; i < views_info.GetSize(); ++i) {
      if (views_info.Get(i).type == WebViewInfo::kPage) {
        *user_client = client.Pass();
        return Status(kOk);
      }
    }
    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
  }
  return Status(kUnknownError, "unable to discover open pages");
}

Status LaunchExistingChromeSession(
    URLRequestContextGetter* context_getter,
    const SyncWebSocketFactory& socket_factory,
    const Capabilities& capabilities,
    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
    scoped_ptr<Chrome>* chrome) {
  Status status(kOk);
  scoped_ptr<DevToolsHttpClient> devtools_client;
  status = WaitForDevToolsAndCheckVersion(
      capabilities.debugger_address, context_getter, socket_factory,
      &devtools_client);
  if (status.IsError()) {
    return Status(kUnknownError, "cannot connect to chrome at " +
                      capabilities.debugger_address.ToString(),
                  status);
  }
  chrome->reset(new ChromeExistingImpl(devtools_client.Pass(),
                                       devtools_event_listeners));
  return Status(kOk);
}

Status LaunchDesktopChrome(
    URLRequestContextGetter* context_getter,
    int port,
    scoped_ptr<PortReservation> port_reservation,
    const SyncWebSocketFactory& socket_factory,
    const Capabilities& capabilities,
    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
    scoped_ptr<Chrome>* chrome) {
  CommandLine command(CommandLine::NO_PROGRAM);
  base::ScopedTempDir user_data_dir;
  base::ScopedTempDir extension_dir;
  std::vector<std::string> extension_bg_pages;
  Status status = PrepareCommandLine(port,
                                     capabilities,
                                     &command,
                                     &user_data_dir,
                                     &extension_dir,
                                     &extension_bg_pages);
  if (status.IsError())
    return status;

  base::LaunchOptions options;

#if defined(OS_LINUX)
  // If minidump path is set in the capability, enable minidump for crashes.
  if (!capabilities.minidump_path.empty()) {
    VLOG(0) << "Minidump generation specified. Will save dumps to: "
            << capabilities.minidump_path;

    options.environ["CHROME_HEADLESS"] = 1;
    options.environ["BREAKPAD_DUMP_LOCATION"] = capabilities.minidump_path;

    if (!command.HasSwitch(kEnableCrashReport))
      command.AppendSwitch(kEnableCrashReport);
  }
#endif

#if !defined(OS_WIN)
  if (!capabilities.log_path.empty())
    options.environ["CHROME_LOG_FILE"] = capabilities.log_path;
  if (capabilities.detach)
    options.new_process_group = true;
#endif

#if defined(OS_POSIX)
  base::FileHandleMappingVector no_stderr;
  int devnull = -1;
  file_util::ScopedFD scoped_devnull(&devnull);
  if (!CommandLine::ForCurrentProcess()->HasSwitch("verbose")) {
    // Redirect stderr to /dev/null, so that Chrome log spew doesn't confuse
    // users.
    devnull = open("/dev/null", O_WRONLY);
    if (devnull == -1)
      return Status(kUnknownError, "couldn't open /dev/null");
    no_stderr.push_back(std::make_pair(devnull, STDERR_FILENO));
    options.fds_to_remap = &no_stderr;
  }
#endif

#if defined(OS_WIN)
  std::string command_string = base::WideToUTF8(command.GetCommandLineString());
#else
  std::string command_string = command.GetCommandLineString();
#endif
  VLOG(0) << "Launching chrome: " << command_string;
  base::ProcessHandle process;
  if (!base::LaunchProcess(command, options, &process))
    return Status(kUnknownError, "chrome failed to start");

  scoped_ptr<DevToolsHttpClient> devtools_client;
  status = WaitForDevToolsAndCheckVersion(
      NetAddress(port), context_getter, socket_factory, &devtools_client);

  if (status.IsError()) {
    int exit_code;
    base::TerminationStatus chrome_status =
        base::GetTerminationStatus(process, &exit_code);
    if (chrome_status != base::TERMINATION_STATUS_STILL_RUNNING) {
      std::string termination_reason;
      switch (chrome_status) {
        case base::TERMINATION_STATUS_NORMAL_TERMINATION:
          termination_reason = "exited normally";
          break;
        case base::TERMINATION_STATUS_ABNORMAL_TERMINATION:
          termination_reason = "exited abnormally";
          break;
        case base::TERMINATION_STATUS_PROCESS_WAS_KILLED:
          termination_reason = "was killed";
          break;
        case base::TERMINATION_STATUS_PROCESS_CRASHED:
          termination_reason = "crashed";
          break;
        default:
          termination_reason = "unknown";
          break;
      }
      return Status(kUnknownError,
                    "Chrome failed to start: " + termination_reason);
    }
    if (!base::KillProcess(process, 0, true)) {
      int exit_code;
      if (base::GetTerminationStatus(process, &exit_code) ==
          base::TERMINATION_STATUS_STILL_RUNNING)
        return Status(kUnknownError, "cannot kill Chrome", status);
    }
    return status;
  }
  scoped_ptr<ChromeDesktopImpl> chrome_desktop(
      new ChromeDesktopImpl(devtools_client.Pass(),
                            devtools_event_listeners,
                            port_reservation.Pass(),
                            process,
                            command,
                            &user_data_dir,
                            &extension_dir));
  for (size_t i = 0; i < extension_bg_pages.size(); ++i) {
    VLOG(0) << "Waiting for extension bg page load: " << extension_bg_pages[i];
    scoped_ptr<WebView> web_view;
    Status status = chrome_desktop->WaitForPageToLoad(
        extension_bg_pages[i], base::TimeDelta::FromSeconds(10), &web_view);
    if (status.IsError()) {
      return Status(kUnknownError,
                    "failed to wait for extension background page to load: " +
                        extension_bg_pages[i],
                    status);
    }
  }
  *chrome = chrome_desktop.Pass();
  return Status(kOk);
}

Status LaunchAndroidChrome(
    URLRequestContextGetter* context_getter,
    int port,
    scoped_ptr<PortReservation> port_reservation,
    const SyncWebSocketFactory& socket_factory,
    const Capabilities& capabilities,
    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
    DeviceManager* device_manager,
    scoped_ptr<Chrome>* chrome) {
  Status status(kOk);
  scoped_ptr<Device> device;
  if (capabilities.android_device_serial.empty()) {
    status = device_manager->AcquireDevice(&device);
  } else {
    status = device_manager->AcquireSpecificDevice(
        capabilities.android_device_serial, &device);
  }
  if (!status.IsOk())
    return status;

  Switches switches(capabilities.switches);
  for (size_t i = 0; i < arraysize(kCommonSwitches); ++i)
    switches.SetSwitch(kCommonSwitches[i]);
  switches.SetSwitch("disable-fre");
  switches.SetSwitch("enable-remote-debugging");
  status = device->SetUp(capabilities.android_package,
                         capabilities.android_activity,
                         capabilities.android_process,
                         switches.ToString(),
                         capabilities.android_use_running_app,
                         port);
  if (!status.IsOk()) {
    device->TearDown();
    return status;
  }

  scoped_ptr<DevToolsHttpClient> devtools_client;
  status = WaitForDevToolsAndCheckVersion(NetAddress(port),
                                          context_getter,
                                          socket_factory,
                                          &devtools_client);
  if (status.IsError())
    return status;

  chrome->reset(new ChromeAndroidImpl(devtools_client.Pass(),
                                      devtools_event_listeners,
                                      port_reservation.Pass(),
                                      device.Pass()));
  return Status(kOk);
}

}  // namespace

Status LaunchChrome(
    URLRequestContextGetter* context_getter,
    const SyncWebSocketFactory& socket_factory,
    DeviceManager* device_manager,
    PortServer* port_server,
    PortManager* port_manager,
    const Capabilities& capabilities,
    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
    scoped_ptr<Chrome>* chrome) {
  if (capabilities.IsExistingBrowser()) {
    return LaunchExistingChromeSession(
        context_getter, socket_factory,
        capabilities, devtools_event_listeners, chrome);
  }

  int port = 0;
  scoped_ptr<PortReservation> port_reservation;
  Status port_status(kOk);
  if (port_server)
    port_status = port_server->ReservePort(&port, &port_reservation);
  else
    port_status = port_manager->ReservePort(&port, &port_reservation);
  if (port_status.IsError())
    return Status(kUnknownError, "cannot reserve port for Chrome", port_status);

  if (capabilities.IsAndroid()) {
    return LaunchAndroidChrome(context_getter,
                               port,
                               port_reservation.Pass(),
                               socket_factory,
                               capabilities,
                               devtools_event_listeners,
                               device_manager,
                               chrome);
  } else {
    return LaunchDesktopChrome(context_getter,
                               port,
                               port_reservation.Pass(),
                               socket_factory,
                               capabilities,
                               devtools_event_listeners,
                               chrome);
  }
}

namespace internal {

void ConvertHexadecimalToIDAlphabet(std::string* id) {
  for (size_t i = 0; i < id->size(); ++i) {
    int val;
    if (base::HexStringToInt(base::StringPiece(id->begin() + i,
                                               id->begin() + i + 1),
                             &val)) {
      (*id)[i] = val + 'a';
    } else {
      (*id)[i] = 'a';
    }
  }
}

std::string GenerateExtensionId(const std::string& input) {
  uint8 hash[16];
  crypto::SHA256HashString(input, hash, sizeof(hash));
  std::string output = StringToLowerASCII(base::HexEncode(hash, sizeof(hash)));
  ConvertHexadecimalToIDAlphabet(&output);
  return output;
}

Status GetExtensionBackgroundPage(const base::DictionaryValue* manifest,
                                  const std::string& id,
                                  std::string* bg_page) {
  std::string bg_page_name;
  bool persistent = true;
  manifest->GetBoolean("background.persistent", &persistent);
  const base::Value* unused_value;
  if (manifest->Get("background.scripts", &unused_value))
    bg_page_name = "_generated_background_page.html";
  manifest->GetString("background.page", &bg_page_name);
  manifest->GetString("background_page", &bg_page_name);
  if (bg_page_name.empty() || !persistent)
    return Status(kOk);
  *bg_page = "chrome-extension://" + id + "/" + bg_page_name;
  return Status(kOk);
}

Status ProcessExtension(const std::string& extension,
                        const base::FilePath& temp_dir,
                        base::FilePath* path,
                        std::string* bg_page) {
  // Decodes extension string.
  // Some WebDriver client base64 encoders follow RFC 1521, which require that
  // 'encoded lines be no more than 76 characters long'. Just remove any
  // newlines.
  std::string extension_base64;
  base::RemoveChars(extension, "\n", &extension_base64);
  std::string decoded_extension;
  if (!base::Base64Decode(extension_base64, &decoded_extension))
    return Status(kUnknownError, "cannot base64 decode");

  // Get extension's ID from public key in crx file.
  // Assumes crx v2. See http://developer.chrome.com/extensions/crx.html.
  std::string key_len_str = decoded_extension.substr(8, 4);
  if (key_len_str.size() != 4)
    return Status(kUnknownError, "cannot extract public key length");
  uint32 key_len = *reinterpret_cast<const uint32*>(key_len_str.c_str());
  std::string public_key = decoded_extension.substr(16, key_len);
  if (key_len != public_key.size())
    return Status(kUnknownError, "invalid public key length");
  std::string public_key_base64;
  base::Base64Encode(public_key, &public_key_base64);
  std::string id = GenerateExtensionId(public_key);

  // Unzip the crx file.
  base::ScopedTempDir temp_crx_dir;
  if (!temp_crx_dir.CreateUniqueTempDir())
    return Status(kUnknownError, "cannot create temp dir");
  base::FilePath extension_crx = temp_crx_dir.path().AppendASCII("temp.crx");
  int size = static_cast<int>(decoded_extension.length());
  if (file_util::WriteFile(extension_crx, decoded_extension.c_str(), size) !=
      size) {
    return Status(kUnknownError, "cannot write file");
  }
  base::FilePath extension_dir = temp_dir.AppendASCII("extension_" + id);
  if (!zip::Unzip(extension_crx, extension_dir))
    return Status(kUnknownError, "cannot unzip");

  // Parse the manifest and set the 'key' if not already present.
  base::FilePath manifest_path(extension_dir.AppendASCII("manifest.json"));
  std::string manifest_data;
  if (!base::ReadFileToString(manifest_path, &manifest_data))
    return Status(kUnknownError, "cannot read manifest");
  scoped_ptr<base::Value> manifest_value(base::JSONReader::Read(manifest_data));
  base::DictionaryValue* manifest;
  if (!manifest_value || !manifest_value->GetAsDictionary(&manifest))
    return Status(kUnknownError, "invalid manifest");

  std::string manifest_key_base64;
  if (manifest->GetString("key", &manifest_key_base64)) {
    // If there is a key in both the header and the manifest, use the key in the
    // manifest. This allows chromedriver users users who generate dummy crxs
    // to set the manifest key and have a consistent ID.
    std::string manifest_key;
    if (!base::Base64Decode(manifest_key_base64, &manifest_key))
      return Status(kUnknownError, "'key' in manifest is not base64 encoded");
    std::string manifest_id = GenerateExtensionId(manifest_key);
    if (id != manifest_id) {
      LOG(WARNING)
          << "Public key in crx header is different from key in manifest"
          << std::endl << "key from header:   " << public_key_base64
          << std::endl << "key from manifest: " << manifest_key_base64
          << std::endl << "generated extension id from header key:   " << id
          << std::endl << "generated extension id from manifest key: "
          << manifest_id;
      id = manifest_id;
    }
  } else {
    manifest->SetString("key", public_key_base64);
    base::JSONWriter::Write(manifest, &manifest_data);
    if (file_util::WriteFile(
            manifest_path, manifest_data.c_str(), manifest_data.size()) !=
        static_cast<int>(manifest_data.size())) {
      return Status(kUnknownError, "cannot add 'key' to manifest");
    }
  }

  // Get extension's background page URL, if there is one.
  std::string bg_page_tmp;
  Status status = GetExtensionBackgroundPage(manifest, id, &bg_page_tmp);
  if (status.IsError())
    return status;

  *path = extension_dir;
  if (bg_page_tmp.size())
    *bg_page = bg_page_tmp;
  return Status(kOk);
}

void UpdateExtensionSwitch(Switches* switches,
                           const char name[],
                           const base::FilePath::StringType& extension) {
  base::FilePath::StringType value = switches->GetSwitchValueNative(name);
  if (value.length())
    value += FILE_PATH_LITERAL(",");
  value += extension;
  switches->SetSwitch(name, value);
}

Status ProcessExtensions(const std::vector<std::string>& extensions,
                         const base::FilePath& temp_dir,
                         bool include_automation_extension,
                         Switches* switches,
                         std::vector<std::string>* bg_pages) {
  std::vector<std::string> bg_pages_tmp;
  std::vector<base::FilePath::StringType> extension_paths;
  for (size_t i = 0; i < extensions.size(); ++i) {
    base::FilePath path;
    std::string bg_page;
    Status status = ProcessExtension(extensions[i], temp_dir, &path, &bg_page);
    if (status.IsError()) {
      return Status(
          kUnknownError,
          base::StringPrintf("cannot process extension #%" PRIuS, i + 1),
          status);
    }
    extension_paths.push_back(path.value());
    if (bg_page.length())
      bg_pages_tmp.push_back(bg_page);
  }

  if (include_automation_extension) {
    base::FilePath automation_extension;
    Status status = UnpackAutomationExtension(temp_dir, &automation_extension);
    if (status.IsError())
      return status;
    if (switches->HasSwitch("disable-extensions")) {
      UpdateExtensionSwitch(switches, "load-component-extension",
                            automation_extension.value());
    } else {
      extension_paths.push_back(automation_extension.value());
    }
  }

  if (extension_paths.size()) {
    base::FilePath::StringType extension_paths_value = JoinString(
        extension_paths, FILE_PATH_LITERAL(','));
    UpdateExtensionSwitch(switches, "load-extension", extension_paths_value);
  }
  bg_pages->swap(bg_pages_tmp);
  return Status(kOk);
}

Status WritePrefsFile(
    const std::string& template_string,
    const base::DictionaryValue* custom_prefs,
    const base::FilePath& path) {
  int code;
  std::string error_msg;
  scoped_ptr<base::Value> template_value(base::JSONReader::ReadAndReturnError(
          template_string, 0, &code, &error_msg));
  base::DictionaryValue* prefs;
  if (!template_value || !template_value->GetAsDictionary(&prefs)) {
    return Status(kUnknownError,
                  "cannot parse internal JSON template: " + error_msg);
  }

  if (custom_prefs) {
    for (base::DictionaryValue::Iterator it(*custom_prefs); !it.IsAtEnd();
         it.Advance()) {
      prefs->Set(it.key(), it.value().DeepCopy());
    }
  }

  std::string prefs_str;
  base::JSONWriter::Write(prefs, &prefs_str);
  VLOG(0) << "Populating " << path.BaseName().value()
          << " file: " << PrettyPrintValue(*prefs);
  if (static_cast<int>(prefs_str.length()) != file_util::WriteFile(
          path, prefs_str.c_str(), prefs_str.length())) {
    return Status(kUnknownError, "failed to write prefs file");
  }
  return Status(kOk);
}

Status PrepareUserDataDir(
    const base::FilePath& user_data_dir,
    const base::DictionaryValue* custom_prefs,
    const base::DictionaryValue* custom_local_state) {
  base::FilePath default_dir = user_data_dir.AppendASCII("Default");
  if (!base::CreateDirectory(default_dir))
    return Status(kUnknownError, "cannot create default profile directory");

  Status status = WritePrefsFile(
      kPreferences,
      custom_prefs,
      default_dir.AppendASCII("Preferences"));
  if (status.IsError())
    return status;

  status = WritePrefsFile(
      kLocalState,
      custom_local_state,
      user_data_dir.AppendASCII("Local State"));
  if (status.IsError())
    return status;

  // Write empty "First Run" file, otherwise Chrome will wipe the default
  // profile that was written.
  if (file_util::WriteFile(
          user_data_dir.AppendASCII("First Run"), "", 0) != 0) {
    return Status(kUnknownError, "failed to write first run file");
  }
  return Status(kOk);
}

}  // namespace internal