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