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 "chromeos/dbus/dbus_thread_manager.h"
12#include "chromeos/dbus/image_burner_client.h"
13#include "chromeos/network/network_state.h"
14#include "chromeos/network/network_state_handler.h"
15#include "chromeos/system/statistics_provider.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
30const char kConfigFileUrl[] =
31    "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf";
32const char kTempImageFolderName[] = "chromeos_image";
33
34const char kImageZipFileName[] = "chromeos_image.bin.zip";
35
36const int64 kBytesImageDownloadProgressReportInterval = 10240;
37
38BurnManager* g_burn_manager = NULL;
39
40// Cretes a directory and calls |callback| with the result on UI thread.
41void CreateDirectory(const base::FilePath& path,
42                     base::Callback<void(bool success)> callback) {
43  const bool success = base::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|.
50void 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
61const char kName[] = "name";
62const char kHwid[] = "hwid";
63const char kFileName[] = "file";
64const char kUrl[] = "url";
65
66////////////////////////////////////////////////////////////////////////////////
67//
68// ConfigFile
69//
70////////////////////////////////////////////////////////////////////////////////
71ConfigFile::ConfigFile() {
72}
73
74ConfigFile::ConfigFile(const std::string& file_content) {
75  reset(file_content);
76}
77
78ConfigFile::~ConfigFile() {
79}
80
81void 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
105void ConfigFile::clear() {
106  config_struct_.clear();
107}
108
109const 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 base::EmptyString();
124      }
125    }
126  }
127
128  return base::EmptyString();
129}
130
131// Check if last block has a hwid associated with it, and erase it if it
132// doesn't,
133void ConfigFile::DeleteLastBlockIfHasNoHwid() {
134  if (!config_struct_.empty() && config_struct_.back().hwids.empty()) {
135    config_struct_.pop_back();
136  }
137}
138
139void 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
167ConfigFile::ConfigFileBlock::ConfigFileBlock() {
168}
169
170ConfigFile::ConfigFileBlock::~ConfigFileBlock() {
171}
172
173////////////////////////////////////////////////////////////////////////////////
174//
175// StateMachine
176//
177////////////////////////////////////////////////////////////////////////////////
178StateMachine::StateMachine()
179    : download_started_(false),
180      download_finished_(false),
181      state_(INITIAL) {
182}
183
184StateMachine::~StateMachine() {
185}
186
187void 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
197void StateMachine::OnSuccess() {
198  if (state_ == INITIAL)
199    return;
200  state_ = INITIAL;
201  OnStateChanged();
202}
203
204////////////////////////////////////////////////////////////////////////////////
205//
206// BurnManager
207//
208////////////////////////////////////////////////////////////////////////////////
209
210BurnManager::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
239BurnManager::~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
251void 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
263void 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
274BurnManager* BurnManager::GetInstance() {
275  return g_burn_manager;
276}
277
278void BurnManager::AddObserver(Observer* observer) {
279  observers_.AddObserver(observer);
280}
281
282void BurnManager::RemoveObserver(Observer* observer) {
283  observers_.RemoveObserver(observer);
284}
285
286std::vector<disks::DiskMountManager::Disk> BurnManager::GetBurnableDevices() {
287  return device_handler_.GetBurnableDevices();
288}
289
290void BurnManager::Cancel() {
291  OnError(IDS_IMAGEBURN_USER_ERROR);
292}
293
294void 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
323void 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
337void 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
350base::FilePath BurnManager::GetImageDir() {
351  if (!image_dir_created_)
352    return base::FilePath();
353  return image_dir_;
354}
355
356void 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
372void 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
397void BurnManager::CancelImageFetch() {
398  image_fetcher_.reset();
399}
400
401void 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
431void 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
439void 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
469void 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
495void 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
504void 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
552void 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::kHardwareClassKey, &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
582void 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
606void 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
619void BurnManager::OnBurnImageFail() {
620  UpdateBurnStatus(BURN_FAIL, ImageBurnStatus(0, 0));
621}
622
623void 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
629void 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
635void BurnManager::NotifyDeviceAdded(
636    const disks::DiskMountManager::Disk& disk) {
637  FOR_EACH_OBSERVER(Observer, observers_, OnDeviceAdded(disk));
638}
639
640void 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