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