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 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 = 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|. 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 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, 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::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 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