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.
5 #include "chrome/browser/chromeos/imageburner/burn_manager.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"
23 using content::BrowserThread
;
26 namespace imageburner
{
30 const char kConfigFileUrl
[] =
31 "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf";
32 const char kTempImageFolderName
[] = "chromeos_image";
34 const char kImageZipFileName
[] = "chromeos_image.bin.zip";
36 const int64 kBytesImageDownloadProgressReportInterval
= 10240;
38 BurnManager
* g_burn_manager
= NULL
;
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
));
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();
61 const char kName
[] = "name";
62 const char kHwid
[] = "hwid";
63 const char kFileName
[] = "file";
64 const char kUrl
[] = "url";
66 ////////////////////////////////////////////////////////////////////////////////
70 ////////////////////////////////////////////////////////////////////////////////
71 ConfigFile::ConfigFile() {
74 ConfigFile::ConfigFile(const std::string
& file_content
) {
78 ConfigFile::~ConfigFile() {
81 void ConfigFile::reset(const std::string
& file_content
) {
84 std::vector
<std::string
> lines
;
85 Tokenize(file_content
, "\n", &lines
);
87 std::vector
<std::string
> key_value_pair
;
88 for (size_t i
= 0; i
< lines
.size(); ++i
) {
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())
98 ProcessLine(key_value_pair
);
101 // Make sure last block has at least one hwid associated with it.
102 DeleteLastBlockIfHasNoHwid();
105 void ConfigFile::clear() {
106 config_struct_
.clear();
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();
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
;
123 return base::EmptyString();
128 return base::EmptyString();
131 // Check if last block has a hwid associated with it, and erase it if it
133 void ConfigFile::DeleteLastBlockIfHasNoHwid() {
134 if (!config_struct_
.empty() && config_struct_
.back().hwids
.empty()) {
135 config_struct_
.pop_back();
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);
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())
155 ConfigFileBlock
& last_block
= config_struct_
.back();
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]);
162 // Add new block property.
163 last_block
.properties
.insert(std::make_pair(line
[0], line
[1]));
167 ConfigFile::ConfigFileBlock::ConfigFileBlock() {
170 ConfigFile::ConfigFileBlock::~ConfigFileBlock() {
173 ////////////////////////////////////////////////////////////////////////////////
177 ////////////////////////////////////////////////////////////////////////////////
178 StateMachine::StateMachine()
179 : download_started_(false),
180 download_finished_(false),
184 StateMachine::~StateMachine() {
187 void StateMachine::OnError(int error_message_id
) {
188 if (state_
== INITIAL
)
190 if (!download_finished_
)
191 download_started_
= false;
194 FOR_EACH_OBSERVER(Observer
, observers_
, OnError(error_message_id
));
197 void StateMachine::OnSuccess() {
198 if (state_
== INITIAL
)
204 ////////////////////////////////////////////////////////////////////////////////
208 ////////////////////////////////////////////////////////////////////////////////
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),
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(
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()));
239 BurnManager::~BurnManager() {
240 if (image_dir_created_
) {
241 base::DeleteFile(image_dir_
, true);
243 if (NetworkHandler::IsInitialized()) {
244 NetworkHandler::Get()->network_state_handler()->RemoveObserver(
247 DBusThreadManager::Get()->GetImageBurnerClient()->ResetEventHandlers();
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";
258 g_burn_manager
= new BurnManager(downloads_directory
, context_getter
);
259 VLOG(1) << "BurnManager initialized";
263 void BurnManager::Shutdown() {
264 if (!g_burn_manager
) {
265 LOG(WARNING
) << "BurnManager::Shutdown() called with NULL manager";
268 delete g_burn_manager
;
269 g_burn_manager
= NULL
;
270 VLOG(1) << "BurnManager Shutdown completed";
274 BurnManager
* BurnManager::GetInstance() {
275 return g_burn_manager
;
278 void BurnManager::AddObserver(Observer
* observer
) {
279 observers_
.AddObserver(observer
);
282 void BurnManager::RemoveObserver(Observer
* observer
) {
283 observers_
.RemoveObserver(observer
);
286 std::vector
<disks::DiskMountManager::Disk
> BurnManager::GetBurnableDevices() {
287 return device_handler_
.GetBurnableDevices();
290 void BurnManager::Cancel() {
291 OnError(IDS_IMAGEBURN_USER_ERROR
);
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
) {
300 // Remember burner state, since it will be reset after OnError call.
301 StateMachine::State state
= state_machine_
->state();
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
);
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
) {
315 } else if (state
== StateMachine::BURNING
) {
316 // Burn library doesn't send cancelled signal upon CancelBurnImage
323 void BurnManager::CreateImageDir() {
324 if (!image_dir_created_
) {
325 BrowserThread::PostBlockingPoolTask(
327 base::Bind(CreateDirectory
,
329 base::Bind(&BurnManager::OnImageDirCreated
,
330 weak_ptr_factory_
.GetWeakPtr())));
332 const bool success
= true;
333 OnImageDirCreated(success
);
337 void BurnManager::OnImageDirCreated(bool success
) {
339 // Failed to create the directory. Finish the burning process
340 // with failure state.
341 OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR
);
345 image_dir_created_
= true;
346 zip_image_file_path_
= image_dir_
.Append(kImageZipFileName
);
350 base::FilePath
BurnManager::GetImageDir() {
351 if (!image_dir_created_
)
352 return base::FilePath();
356 void BurnManager::FetchConfigFile() {
357 if (config_file_fetched_
) {
358 // The config file is already fetched. So start to fetch the image.
363 if (config_fetcher_
.get())
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();
372 void BurnManager::FetchImage() {
373 if (state_machine_
->download_finished()) {
378 if (state_machine_
->download_started()) {
379 // The image downloading is already started. Do nothing.
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
,
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();
394 state_machine_
->OnDownloadStarted();
397 void BurnManager::CancelImageFetch() {
398 image_fetcher_
.reset();
401 void BurnManager::DoBurn() {
402 if (state_machine_
->state() == StateMachine::BURNING
)
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.
409 UpdateBurnStatus(UNZIP_STARTED
, ImageBurnStatus());
413 source_image_path_
.clear();
417 UpdateBurnStatus(UNZIP_STARTED
, ImageBurnStatus());
419 const bool task_is_slow
= true;
420 scoped_refptr
<base::RefCountedString
> result(new base::RefCountedString
);
421 base::WorkerPool::PostTaskAndReply(
423 base::Bind(UnzipImage
, zip_image_file_path_
, image_file_name_
, result
),
424 base::Bind(&BurnManager::OnImageUnzipped
,
425 weak_ptr_factory_
.GetWeakPtr(),
428 state_machine_
->OnBurnStarted();
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.
435 block_burn_signals_
= true;
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.
443 source
->GetStatus().status() == net::URLRequestStatus::SUCCESS
;
445 if (source
== config_fetcher_
.get()) {
446 // Handler for the config file fetcher.
449 config_fetcher_
->GetResponseAsString(&data
);
450 config_fetcher_
.reset();
451 ConfigFileFetched(success
, data
);
455 if (source
== image_fetcher_
.get()) {
456 // Handler for the image file fetcher.
457 state_machine_
->OnDownloadFinished();
459 OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR
);
469 void BurnManager::OnURLFetchDownloadProgress(const net::URLFetcher
* source
,
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
;
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
;
484 // TODO(hidehiko): We should be able to clean the state check here.
485 if (state_machine_
->state() == StateMachine::DOWNLOADING
) {
487 Observer
, observers_
,
488 OnProgressWithRemainingTime(
489 DOWNLOADING
, current
, total
, estimated_remaining_time
));
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());
500 if (state_machine_
->state() == StateMachine::DOWNLOADING
&& !network
)
501 OnError(IDS_IMAGEBURN_NETWORK_ERROR
);
504 void BurnManager::UpdateBurnStatus(BurnEvent event
,
505 const ImageBurnStatus
& status
) {
509 if (event
== BURN_FAIL
|| event
== BURN_SUCCESS
) {
511 if (block_burn_signals_
) {
512 block_burn_signals_
= false;
517 if (block_burn_signals_
&& event
== BURN_UPDATE
)
523 // The burning task is successfully done.
526 state_machine_
->OnSuccess();
527 FOR_EACH_OBSERVER(Observer
, observers_
, OnSuccess());
530 OnError(IDS_IMAGEBURN_BURN_ERROR
);
534 Observer
, observers_
,
535 OnProgress(BURNING
, status
.amount_burnt
, status
.total_size
));
538 FOR_EACH_OBSERVER(Observer
, observers_
, OnProgress(UNZIPPING
, 0, 0));
541 OnError(IDS_IMAGEBURN_EXTRACTING_ERROR
);
552 void BurnManager::ConfigFileFetched(bool fetched
, const std::string
& content
) {
553 if (config_file_fetched_
)
556 // Get image file name and image download URL.
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
));
566 if (fetched
&& !image_file_name_
.empty() && !image_download_url_
.is_empty()) {
567 config_file_fetched_
= true;
570 image_file_name_
.clear();
571 image_download_url_
= GURL();
575 OnError(IDS_IMAGEBURN_DOWNLOAD_ERROR
);
582 void BurnManager::OnImageUnzipped(
583 scoped_refptr
<base::RefCountedString
> source_image_file
) {
584 source_image_path_
= base::FilePath(source_image_file
->data());
586 bool success
= !source_image_path_
.empty();
587 UpdateBurnStatus(success
? UNZIP_COMPLETE
: UNZIP_FAIL
, ImageBurnStatus());
600 chromeos::disks::DiskMountManager::GetInstance()->UnmountDeviceRecursively(
601 target_device_path_
.value(),
602 base::Bind(&BurnManager::OnDevicesUnmounted
,
603 weak_ptr_factory_
.GetWeakPtr()));
606 void BurnManager::OnDevicesUnmounted(bool success
) {
608 UpdateBurnStatus(BURN_FAIL
, ImageBurnStatus(0, 0));
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()));
619 void BurnManager::OnBurnImageFail() {
620 UpdateBurnStatus(BURN_FAIL
, ImageBurnStatus(0, 0));
623 void BurnManager::OnBurnFinished(const std::string
& target_path
,
625 const std::string
& error
) {
626 UpdateBurnStatus(success
? BURN_SUCCESS
: BURN_FAIL
, ImageBurnStatus(0, 0));
629 void BurnManager::OnBurnProgressUpdate(const std::string
& target_path
,
632 UpdateBurnStatus(BURN_UPDATE
, ImageBurnStatus(amount_burnt
, total_size
));
635 void BurnManager::NotifyDeviceAdded(
636 const disks::DiskMountManager::Disk
& disk
) {
637 FOR_EACH_OBSERVER(Observer
, observers_
, OnDeviceAdded(disk
));
640 void BurnManager::NotifyDeviceRemoved(
641 const disks::DiskMountManager::Disk
& disk
) {
642 FOR_EACH_OBSERVER(Observer
, observers_
, OnDeviceRemoved(disk
));
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
649 // TODO(hidehiko): Clean this up after refactoring.
650 OnError(IDS_IMAGEBURN_DEVICE_NOT_FOUND_ERROR
);
654 } // namespace imageburner
655 } // namespace chromeos