root/chrome/browser/chromeos/imageburner/burn_manager.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. CreateDirectory
  2. UnzipImage
  3. reset
  4. clear
  5. GetProperty
  6. DeleteLastBlockIfHasNoHwid
  7. ProcessLine
  8. state_
  9. OnError
  10. OnSuccess
  11. weak_ptr_factory_
  12. Initialize
  13. Shutdown
  14. GetInstance
  15. AddObserver
  16. RemoveObserver
  17. GetBurnableDevices
  18. Cancel
  19. OnError
  20. CreateImageDir
  21. OnImageDirCreated
  22. GetImageDir
  23. FetchConfigFile
  24. FetchImage
  25. CancelImageFetch
  26. DoBurn
  27. CancelBurnImage
  28. OnURLFetchComplete
  29. OnURLFetchDownloadProgress
  30. DefaultNetworkChanged
  31. UpdateBurnStatus
  32. ConfigFileFetched
  33. OnImageUnzipped
  34. OnDevicesUnmounted
  35. OnBurnImageFail
  36. OnBurnFinished
  37. OnBurnProgressUpdate
  38. NotifyDeviceAdded
  39. NotifyDeviceRemoved

// 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/chromeos/imageburner/burn_manager.h"

#include "base/bind.h"
#include "base/file_util.h"
#include "base/strings/string_util.h"
#include "base/threading/worker_pool.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/image_burner_client.h"
#include "chromeos/network/network_state.h"
#include "chromeos/network/network_state_handler.h"
#include "chromeos/system/statistics_provider.h"
#include "content/public/browser/browser_thread.h"
#include "grit/generated_resources.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
#include "third_party/zlib/google/zip.h"

using content::BrowserThread;

namespace chromeos {
namespace imageburner {

namespace {

const char kConfigFileUrl[] =
    "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf";
const char kTempImageFolderName[] = "chromeos_image";

const char kImageZipFileName[] = "chromeos_image.bin.zip";

const int64 kBytesImageDownloadProgressReportInterval = 10240;

BurnManager* g_burn_manager = NULL;

// Cretes a directory and calls |callback| with the result on UI thread.
void CreateDirectory(const base::FilePath& path,
                     base::Callback<void(bool success)> callback) {
  const bool success = base::CreateDirectory(path);
  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
                          base::Bind(callback, success));
}

// Unzips |source_zip_file| and sets the filename of the unzipped image to
// |source_image_file|.
void UnzipImage(const base::FilePath& source_zip_file,
                const std::string& image_name,
                scoped_refptr<base::RefCountedString> source_image_file) {
  if (zip::Unzip(source_zip_file, source_zip_file.DirName())) {
    source_image_file->data() =
        source_zip_file.DirName().Append(image_name).value();
  }
}

}  // namespace

const char kName[] = "name";
const char kHwid[] = "hwid";
const char kFileName[] = "file";
const char kUrl[] = "url";

////////////////////////////////////////////////////////////////////////////////
//
// ConfigFile
//
////////////////////////////////////////////////////////////////////////////////
ConfigFile::ConfigFile() {
}

ConfigFile::ConfigFile(const std::string& file_content) {
  reset(file_content);
}

ConfigFile::~ConfigFile() {
}

void ConfigFile::reset(const std::string& file_content) {
  clear();

  std::vector<std::string> lines;
  Tokenize(file_content, "\n", &lines);

  std::vector<std::string> key_value_pair;
  for (size_t i = 0; i < lines.size(); ++i) {
    if (lines[i].empty())
      continue;

    key_value_pair.clear();
    Tokenize(lines[i], "=", &key_value_pair);
    // Skip lines that don't contain key-value pair and lines without a key.
    if (key_value_pair.size() != 2 || key_value_pair[0].empty())
      continue;

    ProcessLine(key_value_pair);
  }

  // Make sure last block has at least one hwid associated with it.
  DeleteLastBlockIfHasNoHwid();
}

void ConfigFile::clear() {
  config_struct_.clear();
}

const std::string& ConfigFile::GetProperty(
    const std::string& property_name,
    const std::string& hwid) const {
  // We search for block that has desired hwid property, and if we find it, we
  // return its property_name property.
  for (BlockList::const_iterator block_it = config_struct_.begin();
       block_it != config_struct_.end();
       ++block_it) {
    if (block_it->hwids.find(hwid) != block_it->hwids.end()) {
      PropertyMap::const_iterator property =
          block_it->properties.find(property_name);
      if (property != block_it->properties.end()) {
        return property->second;
      } else {
        return base::EmptyString();
      }
    }
  }

  return base::EmptyString();
}

// Check if last block has a hwid associated with it, and erase it if it
// doesn't,
void ConfigFile::DeleteLastBlockIfHasNoHwid() {
  if (!config_struct_.empty() && config_struct_.back().hwids.empty()) {
    config_struct_.pop_back();
  }
}

void ConfigFile::ProcessLine(const std::vector<std::string>& line) {
  // If line contains name key, new image block is starting, so we have to add
  // new entry to our data structure.
  if (line[0] == kName) {
    // If there was no hardware class defined for previous block, we can
    // disregard is since we won't be abble to access any of its properties
    // anyway. This should not happen, but let's be defensive.
    DeleteLastBlockIfHasNoHwid();
    config_struct_.resize(config_struct_.size() + 1);
  }

  // If we still haven't added any blocks to data struct, we disregard this
  // line. Again, this should never happen.
  if (config_struct_.empty())
    return;

  ConfigFileBlock& last_block = config_struct_.back();

  if (line[0] == kHwid) {
    // Check if line contains hwid property. If so, add it to set of hwids
    // associated with current block.
    last_block.hwids.insert(line[1]);
  } else {
    // Add new block property.
    last_block.properties.insert(std::make_pair(line[0], line[1]));
  }
}

ConfigFile::ConfigFileBlock::ConfigFileBlock() {
}

ConfigFile::ConfigFileBlock::~ConfigFileBlock() {
}

////////////////////////////////////////////////////////////////////////////////
//
// StateMachine
//
////////////////////////////////////////////////////////////////////////////////
StateMachine::StateMachine()
    : download_started_(false),
      download_finished_(false),
      state_(INITIAL) {
}

StateMachine::~StateMachine() {
}

void StateMachine::OnError(int error_message_id) {
  if (state_ == INITIAL)
    return;
  if (!download_finished_)
    download_started_ = false;

  state_ = INITIAL;
  FOR_EACH_OBSERVER(Observer, observers_, OnError(error_message_id));
}

void StateMachine::OnSuccess() {
  if (state_ == INITIAL)
    return;
  state_ = INITIAL;
  OnStateChanged();
}

////////////////////////////////////////////////////////////////////////////////
//
// BurnManager
//
////////////////////////////////////////////////////////////////////////////////

BurnManager::BurnManager(
    const base::FilePath& downloads_directory,
    scoped_refptr<net::URLRequestContextGetter> context_getter)
    : device_handler_(disks::DiskMountManager::GetInstance()),
      image_dir_created_(false),
      unzipping_(false),
      cancelled_(false),
      burning_(false),
      block_burn_signals_(false),
      image_dir_(downloads_directory.Append(kTempImageFolderName)),
      config_file_url_(kConfigFileUrl),
      config_file_fetched_(false),
      state_machine_(new StateMachine()),
      url_request_context_getter_(context_getter),
      bytes_image_download_progress_last_reported_(0),
      weak_ptr_factory_(this) {
  NetworkHandler::Get()->network_state_handler()->AddObserver(
      this, FROM_HERE);
  base::WeakPtr<BurnManager> weak_ptr(weak_ptr_factory_.GetWeakPtr());
  device_handler_.SetCallbacks(
      base::Bind(&BurnManager::NotifyDeviceAdded, weak_ptr),
      base::Bind(&BurnManager::NotifyDeviceRemoved, weak_ptr));
  DBusThreadManager::Get()->GetImageBurnerClient()->SetEventHandlers(
      base::Bind(&BurnManager::OnBurnFinished,
                 weak_ptr_factory_.GetWeakPtr()),
      base::Bind(&BurnManager::OnBurnProgressUpdate,
                 weak_ptr_factory_.GetWeakPtr()));
}

BurnManager::~BurnManager() {
  if (image_dir_created_) {
    base::DeleteFile(image_dir_, true);
  }
  if (NetworkHandler::IsInitialized()) {
    NetworkHandler::Get()->network_state_handler()->RemoveObserver(
        this, FROM_HERE);
  }
  DBusThreadManager::Get()->GetImageBurnerClient()->ResetEventHandlers();
}

// static
void BurnManager::Initialize(
    const base::FilePath& downloads_directory,
    scoped_refptr<net::URLRequestContextGetter> context_getter) {
  if (g_burn_manager) {
    LOG(WARNING) << "BurnManager was already initialized";
    return;
  }
  g_burn_manager = new BurnManager(downloads_directory, context_getter);
  VLOG(1) << "BurnManager initialized";
}

// static
void BurnManager::Shutdown() {
  if (!g_burn_manager) {
    LOG(WARNING) << "BurnManager::Shutdown() called with NULL manager";
    return;
  }
  delete g_burn_manager;
  g_burn_manager = NULL;
  VLOG(1) << "BurnManager Shutdown completed";
}

// static
BurnManager* BurnManager::GetInstance() {
  return g_burn_manager;
}

void BurnManager::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void BurnManager::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

std::vector<disks::DiskMountManager::Disk> BurnManager::GetBurnableDevices() {
  return device_handler_.GetBurnableDevices();
}

void BurnManager::Cancel() {
  OnError(IDS_IMAGEBURN_USER_ERROR);
}

void BurnManager::OnError(int message_id) {
  // If we are in intial state, error has already been dispached.
  if (state_machine_->state() == StateMachine::INITIAL) {
    return;
  }

  // Remember burner state, since it will be reset after OnError call.
  StateMachine::State state = state_machine_->state();

  // Dispach error. All hadlers' OnError event will be called before returning
  // from this. This includes us, too.
  state_machine_->OnError(message_id);

  // Cancel and clean up the current task.
  // Note: the cancellation of this class looks not handled correctly.
  // In particular, there seems no clean-up code for creating a temporary
  // directory, or fetching config files. Also, there seems an issue
  // about the cancellation of burning.
  // TODO(hidehiko): Fix the issue.
  if (state  == StateMachine::DOWNLOADING) {
    CancelImageFetch();
  } else if (state == StateMachine::BURNING) {
    // Burn library doesn't send cancelled signal upon CancelBurnImage
    // invokation.
    CancelBurnImage();
  }
  ResetTargetPaths();
}

void BurnManager::CreateImageDir() {
  if (!image_dir_created_) {
    BrowserThread::PostBlockingPoolTask(
        FROM_HERE,
        base::Bind(CreateDirectory,
                   image_dir_,
                   base::Bind(&BurnManager::OnImageDirCreated,
                              weak_ptr_factory_.GetWeakPtr())));
  } else {
    const bool success = true;
    OnImageDirCreated(success);
  }
}

void BurnManager::OnImageDirCreated(bool success) {
  if (!success) {
    // Failed to create the directory. Finish the burning process
    // with failure state.
    OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
    return;
  }

  image_dir_created_ = true;
  zip_image_file_path_ = image_dir_.Append(kImageZipFileName);
  FetchConfigFile();
}

base::FilePath BurnManager::GetImageDir() {
  if (!image_dir_created_)
    return base::FilePath();
  return image_dir_;
}

void BurnManager::FetchConfigFile() {
  if (config_file_fetched_) {
    // The config file is already fetched. So start to fetch the image.
    FetchImage();
    return;
  }

  if (config_fetcher_.get())
    return;

  config_fetcher_.reset(net::URLFetcher::Create(
      config_file_url_, net::URLFetcher::GET, this));
  config_fetcher_->SetRequestContext(url_request_context_getter_.get());
  config_fetcher_->Start();
}

void BurnManager::FetchImage() {
  if (state_machine_->download_finished()) {
    DoBurn();
    return;
  }

  if (state_machine_->download_started()) {
    // The image downloading is already started. Do nothing.
    return;
  }

  tick_image_download_start_ = base::TimeTicks::Now();
  bytes_image_download_progress_last_reported_ = 0;
  image_fetcher_.reset(net::URLFetcher::Create(image_download_url_,
                                               net::URLFetcher::GET,
                                               this));
  image_fetcher_->SetRequestContext(url_request_context_getter_.get());
  image_fetcher_->SaveResponseToFileAtPath(
      zip_image_file_path_,
      BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE));
  image_fetcher_->Start();

  state_machine_->OnDownloadStarted();
}

void BurnManager::CancelImageFetch() {
  image_fetcher_.reset();
}

void BurnManager::DoBurn() {
  if (state_machine_->state() == StateMachine::BURNING)
    return;

  if (unzipping_) {
    // We have unzip in progress, maybe it was "cancelled" before and did not
    // finish yet. In that case, let's pretend cancel did not happen.
    cancelled_ = false;
    UpdateBurnStatus(UNZIP_STARTED, ImageBurnStatus());
    return;
  }

  source_image_path_.clear();

  unzipping_ = true;
  cancelled_ = false;
  UpdateBurnStatus(UNZIP_STARTED, ImageBurnStatus());

  const bool task_is_slow = true;
  scoped_refptr<base::RefCountedString> result(new base::RefCountedString);
  base::WorkerPool::PostTaskAndReply(
      FROM_HERE,
      base::Bind(UnzipImage, zip_image_file_path_, image_file_name_, result),
      base::Bind(&BurnManager::OnImageUnzipped,
                 weak_ptr_factory_.GetWeakPtr(),
                 result),
      task_is_slow);
  state_machine_->OnBurnStarted();
}

void BurnManager::CancelBurnImage() {
  // At the moment, we cannot really stop uzipping or burning. Instead we
  // prevent events from being sent to listeners.
  if (burning_)
    block_burn_signals_ = true;
  cancelled_ = true;
}

void BurnManager::OnURLFetchComplete(const net::URLFetcher* source) {
  // TODO(hidehiko): Split the handler implementation into two, for
  // the config file fetcher and the image file fetcher.
  const bool success =
      source->GetStatus().status() == net::URLRequestStatus::SUCCESS;

  if (source == config_fetcher_.get()) {
    // Handler for the config file fetcher.
    std::string data;
    if (success)
      config_fetcher_->GetResponseAsString(&data);
    config_fetcher_.reset();
    ConfigFileFetched(success, data);
    return;
  }

  if (source == image_fetcher_.get()) {
    // Handler for the image file fetcher.
    state_machine_->OnDownloadFinished();
    if (!success) {
      OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
      return;
    }
    DoBurn();
    return;
  }

  NOTREACHED();
}

void BurnManager::OnURLFetchDownloadProgress(const net::URLFetcher* source,
                                             int64 current,
                                             int64 total) {
  if (source == image_fetcher_.get()) {
    if (current >= bytes_image_download_progress_last_reported_ +
        kBytesImageDownloadProgressReportInterval) {
      bytes_image_download_progress_last_reported_ = current;
      base::TimeDelta estimated_remaining_time;
      if (current > 0) {
        // Extrapolate from the elapsed time.
        const base::TimeDelta elapsed_time =
            base::TimeTicks::Now() - tick_image_download_start_;
        estimated_remaining_time = elapsed_time * (total - current) / current;
      }

      // TODO(hidehiko): We should be able to clean the state check here.
      if (state_machine_->state() == StateMachine::DOWNLOADING) {
        FOR_EACH_OBSERVER(
            Observer, observers_,
            OnProgressWithRemainingTime(
                DOWNLOADING, current, total, estimated_remaining_time));
      }
    }
  }
}

void BurnManager::DefaultNetworkChanged(const NetworkState* network) {
  // TODO(hidehiko): Split this into a class to write tests.
  if (state_machine_->state() == StateMachine::INITIAL && network)
    FOR_EACH_OBSERVER(Observer, observers_, OnNetworkDetected());

  if (state_machine_->state() == StateMachine::DOWNLOADING && !network)
    OnError(IDS_IMAGEBURN_NETWORK_ERROR);
}

void BurnManager::UpdateBurnStatus(BurnEvent event,
                                   const ImageBurnStatus& status) {
  if (cancelled_)
    return;

  if (event == BURN_FAIL || event == BURN_SUCCESS) {
    burning_ = false;
    if (block_burn_signals_) {
      block_burn_signals_ = false;
      return;
    }
  }

  if (block_burn_signals_ && event == BURN_UPDATE)
    return;

  // Notify observers.
  switch (event) {
    case BURN_SUCCESS:
      // The burning task is successfully done.
      // Update the state.
      ResetTargetPaths();
      state_machine_->OnSuccess();
      FOR_EACH_OBSERVER(Observer, observers_, OnSuccess());
      break;
    case BURN_FAIL:
      OnError(IDS_IMAGEBURN_BURN_ERROR);
      break;
    case BURN_UPDATE:
      FOR_EACH_OBSERVER(
          Observer, observers_,
          OnProgress(BURNING, status.amount_burnt, status.total_size));
      break;
    case(UNZIP_STARTED):
      FOR_EACH_OBSERVER(Observer, observers_, OnProgress(UNZIPPING, 0, 0));
      break;
    case UNZIP_FAIL:
      OnError(IDS_IMAGEBURN_EXTRACTING_ERROR);
      break;
    case UNZIP_COMPLETE:
      // We ignore this.
      break;
    default:
      NOTREACHED();
      break;
  }
}

void BurnManager::ConfigFileFetched(bool fetched, const std::string& content) {
  if (config_file_fetched_)
    return;

  // Get image file name and image download URL.
  std::string hwid;
  if (fetched && system::StatisticsProvider::GetInstance()->
      GetMachineStatistic(system::kHardwareClassKey, &hwid)) {
    ConfigFile config_file(content);
    image_file_name_ = config_file.GetProperty(kFileName, hwid);
    image_download_url_ = GURL(config_file.GetProperty(kUrl, hwid));
  }

  // Error check.
  if (fetched && !image_file_name_.empty() && !image_download_url_.is_empty()) {
    config_file_fetched_ = true;
  } else {
    fetched = false;
    image_file_name_.clear();
    image_download_url_ = GURL();
  }

  if (!fetched) {
    OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
    return;
  }

  FetchImage();
}

void BurnManager::OnImageUnzipped(
    scoped_refptr<base::RefCountedString> source_image_file) {
  source_image_path_ = base::FilePath(source_image_file->data());

  bool success = !source_image_path_.empty();
  UpdateBurnStatus(success ? UNZIP_COMPLETE : UNZIP_FAIL, ImageBurnStatus());

  unzipping_ = false;
  if (cancelled_) {
    cancelled_ = false;
    return;
  }

  if (!success)
    return;

  burning_ = true;

  chromeos::disks::DiskMountManager::GetInstance()->UnmountDeviceRecursively(
      target_device_path_.value(),
      base::Bind(&BurnManager::OnDevicesUnmounted,
                 weak_ptr_factory_.GetWeakPtr()));
}

void BurnManager::OnDevicesUnmounted(bool success) {
  if (!success) {
    UpdateBurnStatus(BURN_FAIL, ImageBurnStatus(0, 0));
    return;
  }

  DBusThreadManager::Get()->GetImageBurnerClient()->BurnImage(
      source_image_path_.value(),
      target_file_path_.value(),
      base::Bind(&BurnManager::OnBurnImageFail,
                 weak_ptr_factory_.GetWeakPtr()));
}

void BurnManager::OnBurnImageFail() {
  UpdateBurnStatus(BURN_FAIL, ImageBurnStatus(0, 0));
}

void BurnManager::OnBurnFinished(const std::string& target_path,
                                 bool success,
                                 const std::string& error) {
  UpdateBurnStatus(success ? BURN_SUCCESS : BURN_FAIL, ImageBurnStatus(0, 0));
}

void BurnManager::OnBurnProgressUpdate(const std::string& target_path,
                                       int64 amount_burnt,
                                       int64 total_size) {
  UpdateBurnStatus(BURN_UPDATE, ImageBurnStatus(amount_burnt, total_size));
}

void BurnManager::NotifyDeviceAdded(
    const disks::DiskMountManager::Disk& disk) {
  FOR_EACH_OBSERVER(Observer, observers_, OnDeviceAdded(disk));
}

void BurnManager::NotifyDeviceRemoved(
    const disks::DiskMountManager::Disk& disk) {
  FOR_EACH_OBSERVER(Observer, observers_, OnDeviceRemoved(disk));

  if (target_device_path_.value() == disk.device_path()) {
    // The device is removed during the burning process.
    // Note: in theory, this is not a part of notification, but cancelling
    // the running burning task. However, there is no good place to be in the
    // current code.
    // TODO(hidehiko): Clean this up after refactoring.
    OnError(IDS_IMAGEBURN_DEVICE_NOT_FOUND_ERROR);
  }
}

}  // namespace imageburner
}  // namespace chromeos

/* [<][>][^][v][top][bottom][index][help] */