| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "components/component_updater/background_downloader_win.h" | 5 #include "components/component_updater/background_downloader_win.h" |
| 6 | 6 |
| 7 #include <atlbase.h> | 7 #include <atlbase.h> |
| 8 #include <atlcom.h> | 8 #include <atlcom.h> |
| 9 | 9 |
| 10 #include <stdint.h> | 10 #include <stdint.h> |
| (...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 147 if (FAILED(hr)) | 147 if (FAILED(hr)) |
| 148 return hr; | 148 return hr; |
| 149 | 149 |
| 150 ULONG num_files = 0; | 150 ULONG num_files = 0; |
| 151 hr = enum_files->GetCount(&num_files); | 151 hr = enum_files->GetCount(&num_files); |
| 152 if (FAILED(hr)) | 152 if (FAILED(hr)) |
| 153 return hr; | 153 return hr; |
| 154 | 154 |
| 155 for (ULONG i = 0; i != num_files; ++i) { | 155 for (ULONG i = 0; i != num_files; ++i) { |
| 156 ScopedComPtr<IBackgroundCopyFile> file; | 156 ScopedComPtr<IBackgroundCopyFile> file; |
| 157 if (enum_files->Next(1, file.Receive(), NULL) == S_OK && file) | 157 if (enum_files->Next(1, file.Receive(), NULL) == S_OK && file.get()) |
| 158 files->push_back(file); | 158 files->push_back(file); |
| 159 } | 159 } |
| 160 | 160 |
| 161 return S_OK; | 161 return S_OK; |
| 162 } | 162 } |
| 163 | 163 |
| 164 // Returns the file name, the url, and some per-file progress information. | 164 // Returns the file name, the url, and some per-file progress information. |
| 165 // The function out parameters can be NULL if that data is not requested. | 165 // The function out parameters can be NULL if that data is not requested. |
| 166 HRESULT GetJobFileProperties(IBackgroundCopyFile* file, | 166 HRESULT GetJobFileProperties(IBackgroundCopyFile* file, |
| 167 base::string16* local_name, | 167 base::string16* local_name, |
| (...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 268 ULONG job_count = 0; | 268 ULONG job_count = 0; |
| 269 hr = enum_jobs->GetCount(&job_count); | 269 hr = enum_jobs->GetCount(&job_count); |
| 270 if (FAILED(hr)) | 270 if (FAILED(hr)) |
| 271 return hr; | 271 return hr; |
| 272 | 272 |
| 273 // Iterate over jobs, run the predicate, and select the job only if | 273 // Iterate over jobs, run the predicate, and select the job only if |
| 274 // the job description matches the component updater jobs. | 274 // the job description matches the component updater jobs. |
| 275 for (ULONG i = 0; i != job_count; ++i) { | 275 for (ULONG i = 0; i != job_count; ++i) { |
| 276 ScopedComPtr<IBackgroundCopyJob> current_job; | 276 ScopedComPtr<IBackgroundCopyJob> current_job; |
| 277 if (enum_jobs->Next(1, current_job.Receive(), NULL) == S_OK && | 277 if (enum_jobs->Next(1, current_job.Receive(), NULL) == S_OK && |
| 278 pred(current_job)) { | 278 pred(current_job.get())) { |
| 279 base::string16 job_description; | 279 base::string16 job_description; |
| 280 hr = GetJobDescription(current_job, &job_description); | 280 hr = GetJobDescription(current_job.get(), &job_description); |
| 281 if (job_description.compare(kJobDescription) == 0) | 281 if (job_description.compare(kJobDescription) == 0) |
| 282 jobs->push_back(current_job); | 282 jobs->push_back(current_job); |
| 283 } | 283 } |
| 284 } | 284 } |
| 285 | 285 |
| 286 return jobs->empty() ? S_FALSE : S_OK; | 286 return jobs->empty() ? S_FALSE : S_OK; |
| 287 } | 287 } |
| 288 | 288 |
| 289 // Compares the job creation time and returns true if the job creation time | 289 // Compares the job creation time and returns true if the job creation time |
| 290 // is older than |num_days|. | 290 // is older than |num_days|. |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 342 *bits_manager = object.Detach(); | 342 *bits_manager = object.Detach(); |
| 343 return S_OK; | 343 return S_OK; |
| 344 } | 344 } |
| 345 | 345 |
| 346 void CleanupJobFiles(IBackgroundCopyJob* job) { | 346 void CleanupJobFiles(IBackgroundCopyJob* job) { |
| 347 std::vector<ScopedComPtr<IBackgroundCopyFile> > files; | 347 std::vector<ScopedComPtr<IBackgroundCopyFile> > files; |
| 348 if (FAILED(GetFilesInJob(job, &files))) | 348 if (FAILED(GetFilesInJob(job, &files))) |
| 349 return; | 349 return; |
| 350 for (size_t i = 0; i != files.size(); ++i) { | 350 for (size_t i = 0; i != files.size(); ++i) { |
| 351 base::string16 local_name; | 351 base::string16 local_name; |
| 352 HRESULT hr(GetJobFileProperties(files[i], &local_name, NULL, NULL)); | 352 HRESULT hr(GetJobFileProperties(files[i].get(), &local_name, NULL, NULL)); |
| 353 if (SUCCEEDED(hr)) | 353 if (SUCCEEDED(hr)) |
| 354 DeleteFileAndEmptyParentDirectory(base::FilePath(local_name)); | 354 DeleteFileAndEmptyParentDirectory(base::FilePath(local_name)); |
| 355 } | 355 } |
| 356 } | 356 } |
| 357 | 357 |
| 358 // Cleans up incompleted jobs that are too old. | 358 // Cleans up incompleted jobs that are too old. |
| 359 HRESULT CleanupStaleJobs( | 359 HRESULT CleanupStaleJobs( |
| 360 base::win::ScopedComPtr<IBackgroundCopyManager> bits_manager) { | 360 base::win::ScopedComPtr<IBackgroundCopyManager> bits_manager) { |
| 361 if (!bits_manager) | 361 if (!bits_manager.get()) |
| 362 return E_FAIL; | 362 return E_FAIL; |
| 363 | 363 |
| 364 static base::Time last_sweep; | 364 static base::Time last_sweep; |
| 365 | 365 |
| 366 const base::TimeDelta time_delta( | 366 const base::TimeDelta time_delta( |
| 367 base::TimeDelta::FromDays(kPurgeStaleJobsIntervalBetweenChecksDays)); | 367 base::TimeDelta::FromDays(kPurgeStaleJobsIntervalBetweenChecksDays)); |
| 368 const base::Time current_time(base::Time::Now()); | 368 const base::Time current_time(base::Time::Now()); |
| 369 if (last_sweep + time_delta > current_time) | 369 if (last_sweep + time_delta > current_time) |
| 370 return S_OK; | 370 return S_OK; |
| 371 | 371 |
| 372 last_sweep = current_time; | 372 last_sweep = current_time; |
| 373 | 373 |
| 374 std::vector<ScopedComPtr<IBackgroundCopyJob> > jobs; | 374 std::vector<ScopedComPtr<IBackgroundCopyJob> > jobs; |
| 375 HRESULT hr = FindBitsJobIf( | 375 HRESULT hr = FindBitsJobIf( |
| 376 std::bind2nd(JobCreationOlderThanDays(), kPurgeStaleJobsAfterDays), | 376 std::bind2nd(JobCreationOlderThanDays(), kPurgeStaleJobsAfterDays), |
| 377 bits_manager, | 377 bits_manager.get(), &jobs); |
| 378 &jobs); | |
| 379 if (FAILED(hr)) | 378 if (FAILED(hr)) |
| 380 return hr; | 379 return hr; |
| 381 | 380 |
| 382 for (size_t i = 0; i != jobs.size(); ++i) { | 381 for (size_t i = 0; i != jobs.size(); ++i) { |
| 383 jobs[i]->Cancel(); | 382 jobs[i]->Cancel(); |
| 384 CleanupJobFiles(jobs[i]); | 383 CleanupJobFiles(jobs[i].get()); |
| 385 } | 384 } |
| 386 | 385 |
| 387 return S_OK; | 386 return S_OK; |
| 388 } | 387 } |
| 389 | 388 |
| 390 } // namespace | 389 } // namespace |
| 391 | 390 |
| 392 BackgroundDownloader::BackgroundDownloader( | 391 BackgroundDownloader::BackgroundDownloader( |
| 393 scoped_ptr<CrxDownloader> successor, | 392 scoped_ptr<CrxDownloader> successor, |
| 394 net::URLRequestContextGetter* context_getter, | 393 net::URLRequestContextGetter* context_getter, |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 448 timer_->Start(FROM_HERE, | 447 timer_->Start(FROM_HERE, |
| 449 base::TimeDelta::FromSeconds(kJobPollingIntervalSec), | 448 base::TimeDelta::FromSeconds(kJobPollingIntervalSec), |
| 450 this, | 449 this, |
| 451 &BackgroundDownloader::OnDownloading); | 450 &BackgroundDownloader::OnDownloading); |
| 452 } | 451 } |
| 453 | 452 |
| 454 // Called any time the timer fires. | 453 // Called any time the timer fires. |
| 455 void BackgroundDownloader::OnDownloading() { | 454 void BackgroundDownloader::OnDownloading() { |
| 456 DCHECK(task_runner_->RunsTasksOnCurrentThread()); | 455 DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| 457 | 456 |
| 458 DCHECK(job_); | 457 DCHECK(job_.get()); |
| 459 | 458 |
| 460 DCHECK(!is_completed_); | 459 DCHECK(!is_completed_); |
| 461 if (is_completed_) | 460 if (is_completed_) |
| 462 return; | 461 return; |
| 463 | 462 |
| 464 BG_JOB_STATE job_state = BG_JOB_STATE_ERROR; | 463 BG_JOB_STATE job_state = BG_JOB_STATE_ERROR; |
| 465 HRESULT hr = job_->GetState(&job_state); | 464 HRESULT hr = job_->GetState(&job_state); |
| 466 if (FAILED(hr)) { | 465 if (FAILED(hr)) { |
| 467 EndDownload(hr); | 466 EndDownload(hr); |
| 468 return; | 467 return; |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 517 timer_.reset(); | 516 timer_.reset(); |
| 518 | 517 |
| 519 const base::Time download_end_time(base::Time::Now()); | 518 const base::Time download_end_time(base::Time::Now()); |
| 520 const base::TimeDelta download_time = | 519 const base::TimeDelta download_time = |
| 521 download_end_time >= download_start_time_ | 520 download_end_time >= download_start_time_ |
| 522 ? download_end_time - download_start_time_ | 521 ? download_end_time - download_start_time_ |
| 523 : base::TimeDelta(); | 522 : base::TimeDelta(); |
| 524 | 523 |
| 525 int64_t downloaded_bytes = -1; | 524 int64_t downloaded_bytes = -1; |
| 526 int64_t total_bytes = -1; | 525 int64_t total_bytes = -1; |
| 527 GetJobByteCount(job_, &downloaded_bytes, &total_bytes); | 526 GetJobByteCount(job_.get(), &downloaded_bytes, &total_bytes); |
| 528 | 527 |
| 529 if (FAILED(error) && job_) { | 528 if (FAILED(error) && job_.get()) { |
| 530 job_->Cancel(); | 529 job_->Cancel(); |
| 531 CleanupJobFiles(job_); | 530 CleanupJobFiles(job_.get()); |
| 532 } | 531 } |
| 533 | 532 |
| 534 job_ = NULL; | 533 job_ = NULL; |
| 535 | 534 |
| 536 CleanupStaleJobs(bits_manager_); | 535 CleanupStaleJobs(bits_manager_); |
| 537 bits_manager_ = NULL; | 536 bits_manager_ = NULL; |
| 538 | 537 |
| 539 // Consider the url handled if it has been successfully downloaded or a | 538 // Consider the url handled if it has been successfully downloaded or a |
| 540 // 5xx has been received. | 539 // 5xx has been received. |
| 541 const bool is_handled = | 540 const bool is_handled = |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 574 // BITS job by removing it from the BITS queue and making the download | 573 // BITS job by removing it from the BITS queue and making the download |
| 575 // available to the caller. | 574 // available to the caller. |
| 576 void BackgroundDownloader::OnStateTransferred() { | 575 void BackgroundDownloader::OnStateTransferred() { |
| 577 EndDownload(CompleteJob()); | 576 EndDownload(CompleteJob()); |
| 578 } | 577 } |
| 579 | 578 |
| 580 // Called when the job has encountered an error and no further progress can | 579 // Called when the job has encountered an error and no further progress can |
| 581 // be made. Cancels this job and removes it from the BITS queue. | 580 // be made. Cancels this job and removes it from the BITS queue. |
| 582 void BackgroundDownloader::OnStateError() { | 581 void BackgroundDownloader::OnStateError() { |
| 583 HRESULT error_code = S_OK; | 582 HRESULT error_code = S_OK; |
| 584 HRESULT hr = GetJobError(job_, &error_code); | 583 HRESULT hr = GetJobError(job_.get(), &error_code); |
| 585 if (FAILED(hr)) | 584 if (FAILED(hr)) |
| 586 error_code = hr; | 585 error_code = hr; |
| 587 DCHECK(FAILED(error_code)); | 586 DCHECK(FAILED(error_code)); |
| 588 EndDownload(error_code); | 587 EndDownload(error_code); |
| 589 } | 588 } |
| 590 | 589 |
| 591 // Called when the job has encountered a transient error, such as a | 590 // Called when the job has encountered a transient error, such as a |
| 592 // network disconnect, a server error, or some other recoverable error. | 591 // network disconnect, a server error, or some other recoverable error. |
| 593 void BackgroundDownloader::OnStateTransientError() { | 592 void BackgroundDownloader::OnStateTransientError() { |
| 594 // If the job appears to be stuck, handle the transient error as if | 593 // If the job appears to be stuck, handle the transient error as if |
| 595 // it were a final error. This causes the job to be cancelled and a specific | 594 // it were a final error. This causes the job to be cancelled and a specific |
| 596 // error be returned, if the error was available. | 595 // error be returned, if the error was available. |
| 597 if (IsStuck()) { | 596 if (IsStuck()) { |
| 598 OnStateError(); | 597 OnStateError(); |
| 599 return; | 598 return; |
| 600 } | 599 } |
| 601 | 600 |
| 602 // Don't retry at all if the transient error was a 5xx. | 601 // Don't retry at all if the transient error was a 5xx. |
| 603 HRESULT error_code = S_OK; | 602 HRESULT error_code = S_OK; |
| 604 HRESULT hr = GetJobError(job_, &error_code); | 603 HRESULT hr = GetJobError(job_.get(), &error_code); |
| 605 if (SUCCEEDED(hr) && | 604 if (SUCCEEDED(hr) && |
| 606 IsHttpServerError(GetHttpStatusFromBitsError(error_code))) { | 605 IsHttpServerError(GetHttpStatusFromBitsError(error_code))) { |
| 607 OnStateError(); | 606 OnStateError(); |
| 608 return; | 607 return; |
| 609 } | 608 } |
| 610 } | 609 } |
| 611 | 610 |
| 612 void BackgroundDownloader::OnStateQueued() { | 611 void BackgroundDownloader::OnStateQueued() { |
| 613 if (IsStuck()) | 612 if (IsStuck()) |
| 614 EndDownload(E_ABORT); // Return a generic error for now. | 613 EndDownload(E_ABORT); // Return a generic error for now. |
| 615 } | 614 } |
| 616 | 615 |
| 617 void BackgroundDownloader::OnStateTransferring() { | 616 void BackgroundDownloader::OnStateTransferring() { |
| 618 // Resets the baseline for detecting a stuck job since the job is transferring | 617 // Resets the baseline for detecting a stuck job since the job is transferring |
| 619 // data and it is making progress. | 618 // data and it is making progress. |
| 620 job_stuck_begin_time_ = base::Time::Now(); | 619 job_stuck_begin_time_ = base::Time::Now(); |
| 621 | 620 |
| 622 int64_t downloaded_bytes = -1; | 621 int64_t downloaded_bytes = -1; |
| 623 int64_t total_bytes = -1; | 622 int64_t total_bytes = -1; |
| 624 HRESULT hr = GetJobByteCount(job_, &downloaded_bytes, &total_bytes); | 623 HRESULT hr = GetJobByteCount(job_.get(), &downloaded_bytes, &total_bytes); |
| 625 if (FAILED(hr)) | 624 if (FAILED(hr)) |
| 626 return; | 625 return; |
| 627 | 626 |
| 628 Result result; | 627 Result result; |
| 629 result.downloaded_bytes = downloaded_bytes; | 628 result.downloaded_bytes = downloaded_bytes; |
| 630 result.total_bytes = total_bytes; | 629 result.total_bytes = total_bytes; |
| 631 | 630 |
| 632 main_task_runner_->PostTask( | 631 main_task_runner_->PostTask( |
| 633 FROM_HERE, | 632 FROM_HERE, |
| 634 base::Bind(&BackgroundDownloader::OnDownloadProgress, | 633 base::Bind(&BackgroundDownloader::OnDownloadProgress, |
| (...skipping 11 matching lines...) Expand all Loading... |
| 646 void BackgroundDownloader::OnStateAcknowledged() { | 645 void BackgroundDownloader::OnStateAcknowledged() { |
| 647 EndDownload(E_UNEXPECTED); | 646 EndDownload(E_UNEXPECTED); |
| 648 } | 647 } |
| 649 | 648 |
| 650 // Creates or opens a job for the given url and queues it up. Tries to | 649 // Creates or opens a job for the given url and queues it up. Tries to |
| 651 // install a job observer but continues on if an observer can't be set up. | 650 // install a job observer but continues on if an observer can't be set up. |
| 652 HRESULT BackgroundDownloader::QueueBitsJob(const GURL& url) { | 651 HRESULT BackgroundDownloader::QueueBitsJob(const GURL& url) { |
| 653 DCHECK(task_runner_->RunsTasksOnCurrentThread()); | 652 DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| 654 | 653 |
| 655 HRESULT hr = S_OK; | 654 HRESULT hr = S_OK; |
| 656 if (bits_manager_ == NULL) { | 655 if (bits_manager_.get() == NULL) { |
| 657 hr = GetBitsManager(bits_manager_.Receive()); | 656 hr = GetBitsManager(bits_manager_.Receive()); |
| 658 if (FAILED(hr)) | 657 if (FAILED(hr)) |
| 659 return hr; | 658 return hr; |
| 660 } | 659 } |
| 661 | 660 |
| 662 hr = CreateOrOpenJob(url); | 661 hr = CreateOrOpenJob(url); |
| 663 if (FAILED(hr)) | 662 if (FAILED(hr)) |
| 664 return hr; | 663 return hr; |
| 665 | 664 |
| 666 if (hr == S_OK) { | 665 if (hr == S_OK) { |
| 667 hr = InitializeNewJob(url); | 666 hr = InitializeNewJob(url); |
| 668 if (FAILED(hr)) | 667 if (FAILED(hr)) |
| 669 return hr; | 668 return hr; |
| 670 } | 669 } |
| 671 | 670 |
| 672 return job_->Resume(); | 671 return job_->Resume(); |
| 673 } | 672 } |
| 674 | 673 |
| 675 HRESULT BackgroundDownloader::CreateOrOpenJob(const GURL& url) { | 674 HRESULT BackgroundDownloader::CreateOrOpenJob(const GURL& url) { |
| 676 std::vector<ScopedComPtr<IBackgroundCopyJob> > jobs; | 675 std::vector<ScopedComPtr<IBackgroundCopyJob> > jobs; |
| 677 HRESULT hr = FindBitsJobIf( | 676 HRESULT hr = FindBitsJobIf( |
| 678 std::bind2nd(JobFileUrlEqual(), base::SysUTF8ToWide(url.spec())), | 677 std::bind2nd(JobFileUrlEqual(), base::SysUTF8ToWide(url.spec())), |
| 679 bits_manager_, | 678 bits_manager_.get(), &jobs); |
| 680 &jobs); | |
| 681 if (SUCCEEDED(hr) && !jobs.empty()) { | 679 if (SUCCEEDED(hr) && !jobs.empty()) { |
| 682 job_ = jobs.front(); | 680 job_ = jobs.front(); |
| 683 return S_FALSE; | 681 return S_FALSE; |
| 684 } | 682 } |
| 685 | 683 |
| 686 // Use kJobDescription as a temporary job display name until the proper | 684 // Use kJobDescription as a temporary job display name until the proper |
| 687 // display name is initialized later on. | 685 // display name is initialized later on. |
| 688 GUID guid = {0}; | 686 GUID guid = {0}; |
| 689 ScopedComPtr<IBackgroundCopyJob> job; | 687 ScopedComPtr<IBackgroundCopyJob> job; |
| 690 hr = bits_manager_->CreateJob( | 688 hr = bits_manager_->CreateJob( |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 738 base::TimeDelta::FromMinutes(kJobStuckTimeoutMin)); | 736 base::TimeDelta::FromMinutes(kJobStuckTimeoutMin)); |
| 739 return job_stuck_begin_time_ + job_stuck_timeout < base::Time::Now(); | 737 return job_stuck_begin_time_ + job_stuck_timeout < base::Time::Now(); |
| 740 } | 738 } |
| 741 | 739 |
| 742 HRESULT BackgroundDownloader::CompleteJob() { | 740 HRESULT BackgroundDownloader::CompleteJob() { |
| 743 HRESULT hr = job_->Complete(); | 741 HRESULT hr = job_->Complete(); |
| 744 if (FAILED(hr) && hr != BG_S_UNABLE_TO_DELETE_FILES) | 742 if (FAILED(hr) && hr != BG_S_UNABLE_TO_DELETE_FILES) |
| 745 return hr; | 743 return hr; |
| 746 | 744 |
| 747 std::vector<ScopedComPtr<IBackgroundCopyFile> > files; | 745 std::vector<ScopedComPtr<IBackgroundCopyFile> > files; |
| 748 hr = GetFilesInJob(job_, &files); | 746 hr = GetFilesInJob(job_.get(), &files); |
| 749 if (FAILED(hr)) | 747 if (FAILED(hr)) |
| 750 return hr; | 748 return hr; |
| 751 | 749 |
| 752 if (files.empty()) | 750 if (files.empty()) |
| 753 return E_UNEXPECTED; | 751 return E_UNEXPECTED; |
| 754 | 752 |
| 755 base::string16 local_name; | 753 base::string16 local_name; |
| 756 BG_FILE_PROGRESS progress = {0}; | 754 BG_FILE_PROGRESS progress = {0}; |
| 757 hr = GetJobFileProperties(files.front(), &local_name, NULL, &progress); | 755 hr = GetJobFileProperties(files.front().get(), &local_name, NULL, &progress); |
| 758 if (FAILED(hr)) | 756 if (FAILED(hr)) |
| 759 return hr; | 757 return hr; |
| 760 | 758 |
| 761 // Sanity check the post-conditions of a successful download, including | 759 // Sanity check the post-conditions of a successful download, including |
| 762 // the file and job invariants. The byte counts for a job and its file | 760 // the file and job invariants. The byte counts for a job and its file |
| 763 // must match as a job only contains one file. | 761 // must match as a job only contains one file. |
| 764 DCHECK(progress.Completed); | 762 DCHECK(progress.Completed); |
| 765 DCHECK_EQ(progress.BytesTotal, progress.BytesTransferred); | 763 DCHECK_EQ(progress.BytesTotal, progress.BytesTransferred); |
| 766 | 764 |
| 767 response_ = base::FilePath(local_name); | 765 response_ = base::FilePath(local_name); |
| 768 | 766 |
| 769 return S_OK; | 767 return S_OK; |
| 770 } | 768 } |
| 771 | 769 |
| 772 } // namespace component_updater | 770 } // namespace component_updater |
| OLD | NEW |