Home | History | Annotate | Download | only in imageburner
      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/chromeos/imageburner/burn_manager.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/file_util.h"
      9 #include "base/strings/string_util.h"
     10 #include "base/threading/worker_pool.h"
     11 #include "chrome/browser/chromeos/system/statistics_provider.h"
     12 #include "chromeos/dbus/dbus_thread_manager.h"
     13 #include "chromeos/dbus/image_burner_client.h"
     14 #include "chromeos/network/network_state.h"
     15 #include "chromeos/network/network_state_handler.h"
     16 #include "content/public/browser/browser_thread.h"
     17 #include "grit/generated_resources.h"
     18 #include "net/url_request/url_fetcher.h"
     19 #include "net/url_request/url_request_context_getter.h"
     20 #include "net/url_request/url_request_status.h"
     21 #include "third_party/zlib/google/zip.h"
     22 
     23 using content::BrowserThread;
     24 
     25 namespace chromeos {
     26 namespace imageburner {
     27 
     28 namespace {
     29 
     30 const char kConfigFileUrl[] =
     31     "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf";
     32 const char kTempImageFolderName[] = "chromeos_image";
     33 
     34 const char kImageZipFileName[] = "chromeos_image.bin.zip";
     35 
     36 const int64 kBytesImageDownloadProgressReportInterval = 10240;
     37 
     38 BurnManager* g_burn_manager = NULL;
     39 
     40 // Cretes a directory and calls |callback| with the result on UI thread.
     41 void CreateDirectory(const base::FilePath& path,
     42                      base::Callback<void(bool success)> callback) {
     43   const bool success = file_util::CreateDirectory(path);
     44   BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
     45                           base::Bind(callback, success));
     46 }
     47 
     48 // Unzips |source_zip_file| and sets the filename of the unzipped image to
     49 // |source_image_file|.
     50 void UnzipImage(const base::FilePath& source_zip_file,
     51                 const std::string& image_name,
     52                 scoped_refptr<base::RefCountedString> source_image_file) {
     53   if (zip::Unzip(source_zip_file, source_zip_file.DirName())) {
     54     source_image_file->data() =
     55         source_zip_file.DirName().Append(image_name).value();
     56   }
     57 }
     58 
     59 }  // namespace
     60 
     61 const char kName[] = "name";
     62 const char kHwid[] = "hwid";
     63 const char kFileName[] = "file";
     64 const char kUrl[] = "url";
     65 
     66 ////////////////////////////////////////////////////////////////////////////////
     67 //
     68 // ConfigFile
     69 //
     70 ////////////////////////////////////////////////////////////////////////////////
     71 ConfigFile::ConfigFile() {
     72 }
     73 
     74 ConfigFile::ConfigFile(const std::string& file_content) {
     75   reset(file_content);
     76 }
     77 
     78 ConfigFile::~ConfigFile() {
     79 }
     80 
     81 void ConfigFile::reset(const std::string& file_content) {
     82   clear();
     83 
     84   std::vector<std::string> lines;
     85   Tokenize(file_content, "\n", &lines);
     86 
     87   std::vector<std::string> key_value_pair;
     88   for (size_t i = 0; i < lines.size(); ++i) {
     89     if (lines[i].empty())
     90       continue;
     91 
     92     key_value_pair.clear();
     93     Tokenize(lines[i], "=", &key_value_pair);
     94     // Skip lines that don't contain key-value pair and lines without a key.
     95     if (key_value_pair.size() != 2 || key_value_pair[0].empty())
     96       continue;
     97 
     98     ProcessLine(key_value_pair);
     99   }
    100 
    101   // Make sure last block has at least one hwid associated with it.
    102   DeleteLastBlockIfHasNoHwid();
    103 }
    104 
    105 void ConfigFile::clear() {
    106   config_struct_.clear();
    107 }
    108 
    109 const std::string& ConfigFile::GetProperty(
    110     const std::string& property_name,
    111     const std::string& hwid) const {
    112   // We search for block that has desired hwid property, and if we find it, we
    113   // return its property_name property.
    114   for (BlockList::const_iterator block_it = config_struct_.begin();
    115        block_it != config_struct_.end();
    116        ++block_it) {
    117     if (block_it->hwids.find(hwid) != block_it->hwids.end()) {
    118       PropertyMap::const_iterator property =
    119           block_it->properties.find(property_name);
    120       if (property != block_it->properties.end()) {
    121         return property->second;
    122       } else {
    123         return EmptyString();
    124       }
    125     }
    126   }
    127 
    128   return EmptyString();
    129 }
    130 
    131 // Check if last block has a hwid associated with it, and erase it if it
    132 // doesn't,
    133 void ConfigFile::DeleteLastBlockIfHasNoHwid() {
    134   if (!config_struct_.empty() && config_struct_.back().hwids.empty()) {
    135     config_struct_.pop_back();
    136   }
    137 }
    138 
    139 void ConfigFile::ProcessLine(const std::vector<std::string>& line) {
    140   // If line contains name key, new image block is starting, so we have to add
    141   // new entry to our data structure.
    142   if (line[0] == kName) {
    143     // If there was no hardware class defined for previous block, we can
    144     // disregard is since we won't be abble to access any of its properties
    145     // anyway. This should not happen, but let's be defensive.
    146     DeleteLastBlockIfHasNoHwid();
    147     config_struct_.resize(config_struct_.size() + 1);
    148   }
    149 
    150   // If we still haven't added any blocks to data struct, we disregard this
    151   // line. Again, this should never happen.
    152   if (config_struct_.empty())
    153     return;
    154 
    155   ConfigFileBlock& last_block = config_struct_.back();
    156 
    157   if (line[0] == kHwid) {
    158     // Check if line contains hwid property. If so, add it to set of hwids
    159     // associated with current block.
    160     last_block.hwids.insert(line[1]);
    161   } else {
    162     // Add new block property.
    163     last_block.properties.insert(std::make_pair(line[0], line[1]));
    164   }
    165 }
    166 
    167 ConfigFile::ConfigFileBlock::ConfigFileBlock() {
    168 }
    169 
    170 ConfigFile::ConfigFileBlock::~ConfigFileBlock() {
    171 }
    172 
    173 ////////////////////////////////////////////////////////////////////////////////
    174 //
    175 // StateMachine
    176 //
    177 ////////////////////////////////////////////////////////////////////////////////
    178 StateMachine::StateMachine()
    179     : download_started_(false),
    180       download_finished_(false),
    181       state_(INITIAL) {
    182 }
    183 
    184 StateMachine::~StateMachine() {
    185 }
    186 
    187 void StateMachine::OnError(int error_message_id) {
    188   if (state_ == INITIAL)
    189     return;
    190   if (!download_finished_)
    191     download_started_ = false;
    192 
    193   state_ = INITIAL;
    194   FOR_EACH_OBSERVER(Observer, observers_, OnError(error_message_id));
    195 }
    196 
    197 void StateMachine::OnSuccess() {
    198   if (state_ == INITIAL)
    199     return;
    200   state_ = INITIAL;
    201   OnStateChanged();
    202 }
    203 
    204 ////////////////////////////////////////////////////////////////////////////////
    205 //
    206 // BurnManager
    207 //
    208 ////////////////////////////////////////////////////////////////////////////////
    209 
    210 BurnManager::BurnManager(
    211     const base::FilePath& downloads_directory,
    212     scoped_refptr<net::URLRequestContextGetter> context_getter)
    213     : device_handler_(disks::DiskMountManager::GetInstance()),
    214       image_dir_created_(false),
    215       unzipping_(false),
    216       cancelled_(false),
    217       burning_(false),
    218       block_burn_signals_(false),
    219       image_dir_(downloads_directory.Append(kTempImageFolderName)),
    220       config_file_url_(kConfigFileUrl),
    221       config_file_fetched_(false),
    222       state_machine_(new StateMachine()),
    223       url_request_context_getter_(context_getter),
    224       bytes_image_download_progress_last_reported_(0),
    225       weak_ptr_factory_(this) {
    226   NetworkHandler::Get()->network_state_handler()->AddObserver(
    227       this, FROM_HERE);
    228   base::WeakPtr<BurnManager> weak_ptr(weak_ptr_factory_.GetWeakPtr());
    229   device_handler_.SetCallbacks(
    230       base::Bind(&BurnManager::NotifyDeviceAdded, weak_ptr),
    231       base::Bind(&BurnManager::NotifyDeviceRemoved, weak_ptr));
    232   DBusThreadManager::Get()->GetImageBurnerClient()->SetEventHandlers(
    233       base::Bind(&BurnManager::OnBurnFinished,
    234                  weak_ptr_factory_.GetWeakPtr()),
    235       base::Bind(&BurnManager::OnBurnProgressUpdate,
    236                  weak_ptr_factory_.GetWeakPtr()));
    237 }
    238 
    239 BurnManager::~BurnManager() {
    240   if (image_dir_created_) {
    241     base::DeleteFile(image_dir_, true);
    242   }
    243   if (NetworkHandler::IsInitialized()) {
    244     NetworkHandler::Get()->network_state_handler()->RemoveObserver(
    245         this, FROM_HERE);
    246   }
    247   DBusThreadManager::Get()->GetImageBurnerClient()->ResetEventHandlers();
    248 }
    249 
    250 // static
    251 void BurnManager::Initialize(
    252     const base::FilePath& downloads_directory,
    253     scoped_refptr<net::URLRequestContextGetter> context_getter) {
    254   if (g_burn_manager) {
    255     LOG(WARNING) << "BurnManager was already initialized";
    256     return;
    257   }
    258   g_burn_manager = new BurnManager(downloads_directory, context_getter);
    259   VLOG(1) << "BurnManager initialized";
    260 }
    261 
    262 // static
    263 void BurnManager::Shutdown() {
    264   if (!g_burn_manager) {
    265     LOG(WARNING) << "BurnManager::Shutdown() called with NULL manager";
    266     return;
    267   }
    268   delete g_burn_manager;
    269   g_burn_manager = NULL;
    270   VLOG(1) << "BurnManager Shutdown completed";
    271 }
    272 
    273 // static
    274 BurnManager* BurnManager::GetInstance() {
    275   return g_burn_manager;
    276 }
    277 
    278 void BurnManager::AddObserver(Observer* observer) {
    279   observers_.AddObserver(observer);
    280 }
    281 
    282 void BurnManager::RemoveObserver(Observer* observer) {
    283   observers_.RemoveObserver(observer);
    284 }
    285 
    286 std::vector<disks::DiskMountManager::Disk> BurnManager::GetBurnableDevices() {
    287   return device_handler_.GetBurnableDevices();
    288 }
    289 
    290 void BurnManager::Cancel() {
    291   OnError(IDS_IMAGEBURN_USER_ERROR);
    292 }
    293 
    294 void BurnManager::OnError(int message_id) {
    295   // If we are in intial state, error has already been dispached.
    296   if (state_machine_->state() == StateMachine::INITIAL) {
    297     return;
    298   }
    299 
    300   // Remember burner state, since it will be reset after OnError call.
    301   StateMachine::State state = state_machine_->state();
    302 
    303   // Dispach error. All hadlers' OnError event will be called before returning
    304   // from this. This includes us, too.
    305   state_machine_->OnError(message_id);
    306 
    307   // Cancel and clean up the current task.
    308   // Note: the cancellation of this class looks not handled correctly.
    309   // In particular, there seems no clean-up code for creating a temporary
    310   // directory, or fetching config files. Also, there seems an issue
    311   // about the cancellation of burning.
    312   // TODO(hidehiko): Fix the issue.
    313   if (state  == StateMachine::DOWNLOADING) {
    314     CancelImageFetch();
    315   } else if (state == StateMachine::BURNING) {
    316     // Burn library doesn't send cancelled signal upon CancelBurnImage
    317     // invokation.
    318     CancelBurnImage();
    319   }
    320   ResetTargetPaths();
    321 }
    322 
    323 void BurnManager::CreateImageDir() {
    324   if (!image_dir_created_) {
    325     BrowserThread::PostBlockingPoolTask(
    326         FROM_HERE,
    327         base::Bind(CreateDirectory,
    328                    image_dir_,
    329                    base::Bind(&BurnManager::OnImageDirCreated,
    330                               weak_ptr_factory_.GetWeakPtr())));
    331   } else {
    332     const bool success = true;
    333     OnImageDirCreated(success);
    334   }
    335 }
    336 
    337 void BurnManager::OnImageDirCreated(bool success) {
    338   if (!success) {
    339     // Failed to create the directory. Finish the burning process
    340     // with failure state.
    341     OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
    342     return;
    343   }
    344 
    345   image_dir_created_ = true;
    346   zip_image_file_path_ = image_dir_.Append(kImageZipFileName);
    347   FetchConfigFile();
    348 }
    349 
    350 base::FilePath BurnManager::GetImageDir() {
    351   if (!image_dir_created_)
    352     return base::FilePath();
    353   return image_dir_;
    354 }
    355 
    356 void BurnManager::FetchConfigFile() {
    357   if (config_file_fetched_) {
    358     // The config file is already fetched. So start to fetch the image.
    359     FetchImage();
    360     return;
    361   }
    362 
    363   if (config_fetcher_.get())
    364     return;
    365 
    366   config_fetcher_.reset(net::URLFetcher::Create(
    367       config_file_url_, net::URLFetcher::GET, this));
    368   config_fetcher_->SetRequestContext(url_request_context_getter_.get());
    369   config_fetcher_->Start();
    370 }
    371 
    372 void BurnManager::FetchImage() {
    373   if (state_machine_->download_finished()) {
    374     DoBurn();
    375     return;
    376   }
    377 
    378   if (state_machine_->download_started()) {
    379     // The image downloading is already started. Do nothing.
    380     return;
    381   }
    382 
    383   tick_image_download_start_ = base::TimeTicks::Now();
    384   bytes_image_download_progress_last_reported_ = 0;
    385   image_fetcher_.reset(net::URLFetcher::Create(image_download_url_,
    386                                                net::URLFetcher::GET,
    387                                                this));
    388   image_fetcher_->SetRequestContext(url_request_context_getter_.get());
    389   image_fetcher_->SaveResponseToFileAtPath(
    390       zip_image_file_path_,
    391       BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE));
    392   image_fetcher_->Start();
    393 
    394   state_machine_->OnDownloadStarted();
    395 }
    396 
    397 void BurnManager::CancelImageFetch() {
    398   image_fetcher_.reset();
    399 }
    400 
    401 void BurnManager::DoBurn() {
    402   if (state_machine_->state() == StateMachine::BURNING)
    403     return;
    404 
    405   if (unzipping_) {
    406     // We have unzip in progress, maybe it was "cancelled" before and did not
    407     // finish yet. In that case, let's pretend cancel did not happen.
    408     cancelled_ = false;
    409     UpdateBurnStatus(UNZIP_STARTED, ImageBurnStatus());
    410     return;
    411   }
    412 
    413   source_image_path_.clear();
    414 
    415   unzipping_ = true;
    416   cancelled_ = false;
    417   UpdateBurnStatus(UNZIP_STARTED, ImageBurnStatus());
    418 
    419   const bool task_is_slow = true;
    420   scoped_refptr<base::RefCountedString> result(new base::RefCountedString);
    421   base::WorkerPool::PostTaskAndReply(
    422       FROM_HERE,
    423       base::Bind(UnzipImage, zip_image_file_path_, image_file_name_, result),
    424       base::Bind(&BurnManager::OnImageUnzipped,
    425                  weak_ptr_factory_.GetWeakPtr(),
    426                  result),
    427       task_is_slow);
    428   state_machine_->OnBurnStarted();
    429 }
    430 
    431 void BurnManager::CancelBurnImage() {
    432   // At the moment, we cannot really stop uzipping or burning. Instead we
    433   // prevent events from being sent to listeners.
    434   if (burning_)
    435     block_burn_signals_ = true;
    436   cancelled_ = true;
    437 }
    438 
    439 void BurnManager::OnURLFetchComplete(const net::URLFetcher* source) {
    440   // TODO(hidehiko): Split the handler implementation into two, for
    441   // the config file fetcher and the image file fetcher.
    442   const bool success =
    443       source->GetStatus().status() == net::URLRequestStatus::SUCCESS;
    444 
    445   if (source == config_fetcher_.get()) {
    446     // Handler for the config file fetcher.
    447     std::string data;
    448     if (success)
    449       config_fetcher_->GetResponseAsString(&data);
    450     config_fetcher_.reset();
    451     ConfigFileFetched(success, data);
    452     return;
    453   }
    454 
    455   if (source == image_fetcher_.get()) {
    456     // Handler for the image file fetcher.
    457     state_machine_->OnDownloadFinished();
    458     if (!success) {
    459       OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
    460       return;
    461     }
    462     DoBurn();
    463     return;
    464   }
    465 
    466   NOTREACHED();
    467 }
    468 
    469 void BurnManager::OnURLFetchDownloadProgress(const net::URLFetcher* source,
    470                                              int64 current,
    471                                              int64 total) {
    472   if (source == image_fetcher_.get()) {
    473     if (current >= bytes_image_download_progress_last_reported_ +
    474         kBytesImageDownloadProgressReportInterval) {
    475       bytes_image_download_progress_last_reported_ = current;
    476       base::TimeDelta estimated_remaining_time;
    477       if (current > 0) {
    478         // Extrapolate from the elapsed time.
    479         const base::TimeDelta elapsed_time =
    480             base::TimeTicks::Now() - tick_image_download_start_;
    481         estimated_remaining_time = elapsed_time * (total - current) / current;
    482       }
    483 
    484       // TODO(hidehiko): We should be able to clean the state check here.
    485       if (state_machine_->state() == StateMachine::DOWNLOADING) {
    486         FOR_EACH_OBSERVER(
    487             Observer, observers_,
    488             OnProgressWithRemainingTime(
    489                 DOWNLOADING, current, total, estimated_remaining_time));
    490       }
    491     }
    492   }
    493 }
    494 
    495 void BurnManager::DefaultNetworkChanged(const NetworkState* network) {
    496   // TODO(hidehiko): Split this into a class to write tests.
    497   if (state_machine_->state() == StateMachine::INITIAL && network)
    498     FOR_EACH_OBSERVER(Observer, observers_, OnNetworkDetected());
    499 
    500   if (state_machine_->state() == StateMachine::DOWNLOADING && !network)
    501     OnError(IDS_IMAGEBURN_NETWORK_ERROR);
    502 }
    503 
    504 void BurnManager::UpdateBurnStatus(BurnEvent event,
    505                                    const ImageBurnStatus& status) {
    506   if (cancelled_)
    507     return;
    508 
    509   if (event == BURN_FAIL || event == BURN_SUCCESS) {
    510     burning_ = false;
    511     if (block_burn_signals_) {
    512       block_burn_signals_ = false;
    513       return;
    514     }
    515   }
    516 
    517   if (block_burn_signals_ && event == BURN_UPDATE)
    518     return;
    519 
    520   // Notify observers.
    521   switch (event) {
    522     case BURN_SUCCESS:
    523       // The burning task is successfully done.
    524       // Update the state.
    525       ResetTargetPaths();
    526       state_machine_->OnSuccess();
    527       FOR_EACH_OBSERVER(Observer, observers_, OnSuccess());
    528       break;
    529     case BURN_FAIL:
    530       OnError(IDS_IMAGEBURN_BURN_ERROR);
    531       break;
    532     case BURN_UPDATE:
    533       FOR_EACH_OBSERVER(
    534           Observer, observers_,
    535           OnProgress(BURNING, status.amount_burnt, status.total_size));
    536       break;
    537     case(UNZIP_STARTED):
    538       FOR_EACH_OBSERVER(Observer, observers_, OnProgress(UNZIPPING, 0, 0));
    539       break;
    540     case UNZIP_FAIL:
    541       OnError(IDS_IMAGEBURN_EXTRACTING_ERROR);
    542       break;
    543     case UNZIP_COMPLETE:
    544       // We ignore this.
    545       break;
    546     default:
    547       NOTREACHED();
    548       break;
    549   }
    550 }
    551 
    552 void BurnManager::ConfigFileFetched(bool fetched, const std::string& content) {
    553   if (config_file_fetched_)
    554     return;
    555 
    556   // Get image file name and image download URL.
    557   std::string hwid;
    558   if (fetched && system::StatisticsProvider::GetInstance()->
    559       GetMachineStatistic(system::kHardwareClass, &hwid)) {
    560     ConfigFile config_file(content);
    561     image_file_name_ = config_file.GetProperty(kFileName, hwid);
    562     image_download_url_ = GURL(config_file.GetProperty(kUrl, hwid));
    563   }
    564 
    565   // Error check.
    566   if (fetched && !image_file_name_.empty() && !image_download_url_.is_empty()) {
    567     config_file_fetched_ = true;
    568   } else {
    569     fetched = false;
    570     image_file_name_.clear();
    571     image_download_url_ = GURL();
    572   }
    573 
    574   if (!fetched) {
    575     OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR);
    576     return;
    577   }
    578 
    579   FetchImage();
    580 }
    581 
    582 void BurnManager::OnImageUnzipped(
    583     scoped_refptr<base::RefCountedString> source_image_file) {
    584   source_image_path_ = base::FilePath(source_image_file->data());
    585 
    586   bool success = !source_image_path_.empty();
    587   UpdateBurnStatus(success ? UNZIP_COMPLETE : UNZIP_FAIL, ImageBurnStatus());
    588 
    589   unzipping_ = false;
    590   if (cancelled_) {
    591     cancelled_ = false;
    592     return;
    593   }
    594 
    595   if (!success)
    596     return;
    597 
    598   burning_ = true;
    599 
    600   chromeos::disks::DiskMountManager::GetInstance()->UnmountDeviceRecursively(
    601       target_device_path_.value(),
    602       base::Bind(&BurnManager::OnDevicesUnmounted,
    603                  weak_ptr_factory_.GetWeakPtr()));
    604 }
    605 
    606 void BurnManager::OnDevicesUnmounted(bool success) {
    607   if (!success) {
    608     UpdateBurnStatus(BURN_FAIL, ImageBurnStatus(0, 0));
    609     return;
    610   }
    611 
    612   DBusThreadManager::Get()->GetImageBurnerClient()->BurnImage(
    613       source_image_path_.value(),
    614       target_file_path_.value(),
    615       base::Bind(&BurnManager::OnBurnImageFail,
    616                  weak_ptr_factory_.GetWeakPtr()));
    617 }
    618 
    619 void BurnManager::OnBurnImageFail() {
    620   UpdateBurnStatus(BURN_FAIL, ImageBurnStatus(0, 0));
    621 }
    622 
    623 void BurnManager::OnBurnFinished(const std::string& target_path,
    624                                  bool success,
    625                                  const std::string& error) {
    626   UpdateBurnStatus(success ? BURN_SUCCESS : BURN_FAIL, ImageBurnStatus(0, 0));
    627 }
    628 
    629 void BurnManager::OnBurnProgressUpdate(const std::string& target_path,
    630                                        int64 amount_burnt,
    631                                        int64 total_size) {
    632   UpdateBurnStatus(BURN_UPDATE, ImageBurnStatus(amount_burnt, total_size));
    633 }
    634 
    635 void BurnManager::NotifyDeviceAdded(
    636     const disks::DiskMountManager::Disk& disk) {
    637   FOR_EACH_OBSERVER(Observer, observers_, OnDeviceAdded(disk));
    638 }
    639 
    640 void BurnManager::NotifyDeviceRemoved(
    641     const disks::DiskMountManager::Disk& disk) {
    642   FOR_EACH_OBSERVER(Observer, observers_, OnDeviceRemoved(disk));
    643 
    644   if (target_device_path_.value() == disk.device_path()) {
    645     // The device is removed during the burning process.
    646     // Note: in theory, this is not a part of notification, but cancelling
    647     // the running burning task. However, there is no good place to be in the
    648     // current code.
    649     // TODO(hidehiko): Clean this up after refactoring.
    650     OnError(IDS_IMAGEBURN_DEVICE_NOT_FOUND_ERROR);
    651   }
    652 }
    653 
    654 }  // namespace imageburner
    655 }  // namespace chromeos
    656