| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/update_client/background_downloader_win.h" |
| |
| #include <objbase.h> |
| |
| #include <windows.h> |
| |
| #include <shlobj_core.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <winerror.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/functional/function_ref.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/path_service.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_thread_priority.h" |
| #include "base/win/com_init_util.h" |
| #include "base/win/scoped_co_mem.h" |
| #include "components/update_client/task_traits.h" |
| #include "components/update_client/update_client_errors.h" |
| #include "components/update_client/update_client_metrics.h" |
| #include "components/update_client/utils.h" |
| #include "url/gurl.h" |
| |
| // The class BackgroundDownloader in this module is an adapter between |
| // the CrxDownloader interface and the BITS service interfaces. |
| // The interface exposed on the CrxDownloader code runs on the main sequence, |
| // while the BITS specific code runs in a separate sequence bound to a |
| // COM apartment. For every url to download, a BITS job is created, unless |
| // there is already an existing job for that url, in which case, the downloader |
| // connects to it. Once a job is associated with the url, the code looks for |
| // changes in the BITS job state. The checks are triggered by a timer. |
| // The BITS job contains just one file to download. There could only be one |
| // download in progress at a time. If Chrome closes down before the download is |
| // complete, the BITS job remains active and finishes in the background, without |
| // any intervention. The job can be completed next time the code runs, if the |
| // file is still needed, otherwise it will be cleaned up on a periodic basis. |
| // |
| // To list the BITS jobs for a user, use the |bitsadmin| tool. The command line |
| // to do that is: "bitsadmin /list /verbose". Another useful command is |
| // "bitsadmin /info" and provide the job id returned by the previous /list |
| // command. |
| // |
| // Ignoring the suspend/resume issues since this code is not using them, the |
| // job state machine implemented by BITS is something like this: |
| // |
| // Suspended--->Queued--->Connecting---->Transferring--->Transferred |
| // | ^ | | | |
| // | | V V | (complete) |
| // +----------|---------+-----------------+-----+ V |
| // | | | | Acknowledged |
| // | V V | |
| // | Transient Error------->Error | |
| // | | | |(cancel) |
| // | +-------+---------+--->-+ |
| // | V | |
| // | (resume) | | |
| // +------<----------+ +---->Cancelled |
| // |
| // The job is created in the "suspended" state. Once |Resume| is called, |
| // BITS queues up the job, then tries to connect, begins transferring the |
| // job bytes, and moves the job to the "transferred state, after the job files |
| // have been transferred. When calling |Complete| for a job, the job files are |
| // made available to the caller, and the job is moved to the "acknowledged" |
| // state. |
| // At any point, the job can be cancelled, in which case, the job is moved |
| // to the "cancelled state" and the job object is removed from the BITS queue. |
| // Along the way, the job can encounter recoverable and non-recoverable errors. |
| // BITS moves the job to "transient error" or "error", depending on which kind |
| // of error has occured. |
| // If the job has reached the "transient error" state, BITS retries the |
| // job after a certain programmable delay. If the job can't be completed in a |
| // certain time interval, BITS stops retrying and errors the job out. This time |
| // interval is also programmable. |
| // If the job is in either of the error states, the job parameters can be |
| // adjusted to handle the error, after which the job can be resumed, and the |
| // whole cycle starts again. |
| // Jobs that are not touched in 90 days (or a value set by group policy) are |
| // automatically disposed off by BITS. This concludes the brief description of |
| // a job lifetime, according to BITS. |
| // |
| // In addition to how BITS is managing the life time of the job, there are a |
| // couple of special cases defined by the BackgroundDownloader. |
| // First, if the job encounters any of the 5xx HTTP responses, the job is |
| // not retried, in order to avoid DDOS-ing the servers. |
| // Second, there is a simple mechanism to detect stuck jobs, and allow the rest |
| // of the code to move on to trying other urls or trying other components. |
| // Last, after completing a job, irrespective of the outcome, the jobs older |
| // than a week are proactively cleaned up. |
| |
| namespace update_client { |
| namespace { |
| |
| // All jobs created by this module have a specific description so they can |
| // be found at run-time or by using system administration tools. |
| constexpr wchar_t kJobName[] = L"Chrome Component Updater"; |
| |
| // How often the code looks for changes in the BITS job state. |
| constexpr int kJobPollingIntervalSec = 4; |
| |
| // How long BITS waits before retrying a job after the job encountered |
| // a transient error. If this value is not set, the BITS default is 10 minutes. |
| constexpr int kMinimumRetryDelayMin = 1; |
| |
| // How long to wait for stuck jobs. Stuck jobs could be queued for too long, |
| // have trouble connecting, or could be suspended for any reason. |
| constexpr int kJobStuckTimeoutMin = 15; |
| |
| // How long BITS waits before giving up on a job that could not be completed |
| // since the job has encountered its first transient error. If this value is |
| // not set, the BITS default is 14 days. |
| constexpr int kSetNoProgressTimeoutDays = 1; |
| |
| // How often the jobs which were started but not completed for any reason |
| // are cleaned up. Reasons for jobs to be left behind include browser restarts, |
| // system restarts, etc. Also, the check to purge stale jobs only happens |
| // at most once a day. If the job clean up code is not running, the BITS |
| // default policy is to cancel jobs after 90 days of inactivity. |
| constexpr int kPurgeStaleJobsAfterDays = 3; |
| constexpr int kPurgeStaleJobsIntervalBetweenChecksDays = 1; |
| |
| // Number of maximum BITS jobs this downloader can create and queue up. |
| constexpr int kMaxQueuedJobs = 10; |
| |
| // Prefix used for naming the temporary directories for downloads. |
| constexpr base::FilePath::CharType kDownloadDirectoryPrefix[] = |
| FILE_PATH_LITERAL("chrome_BITS_"); |
| constexpr base::FilePath::CharType kDownloadDirectoryPrefixMatcher[] = |
| FILE_PATH_LITERAL("chrome_BITS_*"); |
| |
| // Returns the status code from a given BITS error. |
| int GetHttpStatusFromBitsError(HRESULT error) { |
| // BITS errors are defined in bitsmsg.h. Although not documented, it is |
| // clear that all errors corresponding to http status code have the high |
| // word equal to 0x8019 and the low word equal to the http status code. |
| const int kHttpStatusFirst = 100; // Continue. |
| const int kHttpStatusLast = 505; // Version not supported. |
| bool is_valid = HIWORD(error) == 0x8019 && |
| LOWORD(error) >= kHttpStatusFirst && |
| LOWORD(error) <= kHttpStatusLast; |
| return is_valid ? LOWORD(error) : 0; |
| } |
| |
| // Returns the files in a BITS job. |
| HRESULT GetFilesInJob( |
| const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job, |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyFile>>* files) { |
| Microsoft::WRL::ComPtr<IEnumBackgroundCopyFiles> enum_files; |
| HRESULT hr = job->EnumFiles(&enum_files); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| ULONG num_files = 0; |
| hr = enum_files->GetCount(&num_files); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| for (ULONG i = 0; i != num_files; ++i) { |
| Microsoft::WRL::ComPtr<IBackgroundCopyFile> file; |
| if (enum_files->Next(1, &file, nullptr) == S_OK && file.Get()) { |
| files->push_back(file); |
| } |
| } |
| |
| return S_OK; |
| } |
| |
| // Returns the file name, the url, and some per-file progress information. |
| // The function out parameters can be NULL if that data is not requested. |
| HRESULT GetJobFileProperties( |
| const Microsoft::WRL::ComPtr<IBackgroundCopyFile>& file, |
| std::wstring* local_name, |
| std::wstring* remote_name, |
| BG_FILE_PROGRESS* progress) { |
| if (!file) { |
| return E_FAIL; |
| } |
| |
| HRESULT hr = S_OK; |
| |
| if (local_name) { |
| base::win::ScopedCoMem<wchar_t> name; |
| hr = file->GetLocalName(&name); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| local_name->assign(name); |
| } |
| |
| if (remote_name) { |
| base::win::ScopedCoMem<wchar_t> name; |
| hr = file->GetRemoteName(&name); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| remote_name->assign(name); |
| } |
| |
| if (progress) { |
| BG_FILE_PROGRESS bg_file_progress = {}; |
| hr = file->GetProgress(&bg_file_progress); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| *progress = bg_file_progress; |
| } |
| |
| return hr; |
| } |
| |
| // Returns the number of bytes downloaded and bytes to download for all files |
| // in the job. If the values are not known or if an error has occurred, |
| // a value of -1 is reported. |
| HRESULT GetJobByteCount(const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job, |
| int64_t* downloaded_bytes, |
| int64_t* total_bytes) { |
| *downloaded_bytes = -1; |
| *total_bytes = -1; |
| |
| if (!job) { |
| return E_FAIL; |
| } |
| |
| BG_JOB_PROGRESS job_progress = {}; |
| HRESULT hr = job->GetProgress(&job_progress); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| const uint64_t kMaxNumBytes = |
| static_cast<uint64_t>(std::numeric_limits<int64_t>::max()); |
| if (job_progress.BytesTransferred <= kMaxNumBytes) { |
| *downloaded_bytes = job_progress.BytesTransferred; |
| } |
| |
| if (job_progress.BytesTotal <= kMaxNumBytes && |
| job_progress.BytesTotal != BG_SIZE_UNKNOWN) { |
| *total_bytes = job_progress.BytesTotal; |
| } |
| |
| return S_OK; |
| } |
| |
| HRESULT GetJobDisplayName(const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job, |
| std::wstring* name) { |
| base::win::ScopedCoMem<wchar_t> local_name; |
| const HRESULT hr = job->GetDisplayName(&local_name); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| *name = local_name.get(); |
| return S_OK; |
| } |
| |
| // Returns the job error code in |error_code| if the job is in the transient |
| // or the final error state. Otherwise, the job error is not available and |
| // the function fails. |
| HRESULT GetJobError(const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job, |
| HRESULT* error_code_out) { |
| *error_code_out = S_OK; |
| Microsoft::WRL::ComPtr<IBackgroundCopyError> copy_error; |
| HRESULT hr = job->GetError(©_error); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| BG_ERROR_CONTEXT error_context = BG_ERROR_CONTEXT_NONE; |
| HRESULT error_code = S_OK; |
| hr = copy_error->GetError(&error_context, &error_code); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| *error_code_out = FAILED(error_code) ? error_code : E_FAIL; |
| return S_OK; |
| } |
| |
| // Finds the component updater jobs matching the given predicate. |
| // Returns S_OK if the function has found at least one job, returns S_FALSE if |
| // no job was found, and it returns an error otherwise. |
| template <class Predicate> |
| HRESULT FindBitsJobIf( |
| Predicate pred, |
| const Microsoft::WRL::ComPtr<IBackgroundCopyManager>& bits_manager, |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyJob>>* jobs) { |
| Microsoft::WRL::ComPtr<IEnumBackgroundCopyJobs> enum_jobs; |
| HRESULT hr = bits_manager->EnumJobs(0, &enum_jobs); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| ULONG job_count = 0; |
| hr = enum_jobs->GetCount(&job_count); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| // Iterate over jobs, run the predicate, and select the job only if |
| // the job description matches the component updater jobs. |
| for (ULONG i = 0; i != job_count; ++i) { |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob> current_job; |
| if (enum_jobs->Next(1, ¤t_job, nullptr) == S_OK && |
| pred(current_job)) { |
| std::wstring job_name; |
| hr = GetJobDisplayName(current_job, &job_name); |
| if (job_name.compare(kJobName) == 0) { |
| jobs->push_back(current_job); |
| } |
| } |
| } |
| |
| return jobs->empty() ? S_FALSE : S_OK; |
| } |
| |
| bool JobCreationOlderThanDaysPredicate( |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob> job, |
| int num_days) { |
| BG_JOB_TIMES times = {}; |
| HRESULT hr = job->GetTimes(×); |
| if (FAILED(hr)) { |
| return false; |
| } |
| |
| const base::TimeDelta time_delta(base::Days(num_days)); |
| const base::Time creation_time(base::Time::FromFileTime(times.CreationTime)); |
| |
| return creation_time + time_delta < base::Time::Now(); |
| } |
| |
| bool JobFileUrlEqualPredicate(Microsoft::WRL::ComPtr<IBackgroundCopyJob> job, |
| const GURL& url) { |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyFile>> files; |
| HRESULT hr = GetFilesInJob(job, &files); |
| if (FAILED(hr)) { |
| return false; |
| } |
| |
| for (size_t i = 0; i != files.size(); ++i) { |
| base::win::ScopedCoMem<wchar_t> remote_name; |
| if (SUCCEEDED(files[i]->GetRemoteName(&remote_name)) && |
| url == GURL(base::SysWideToUTF8(remote_name.get()))) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Creates an instance of the BITS manager. |
| HRESULT CreateBitsManager( |
| Microsoft::WRL::ComPtr<IBackgroundCopyManager>* bits_manager) { |
| Microsoft::WRL::ComPtr<IBackgroundCopyManager> local_bits_manager; |
| HRESULT hr; |
| { |
| // CoCreateInstance may acquire the loader lock to load a library. Doing it |
| // at background priority can cause a priority inversion with the main |
| // thread, perceived as a hang by the user. |
| // SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY() mitigates this problem |
| // by boosting the thread's priority. See crbug.com/1295941. |
| SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY(); |
| hr = ::CoCreateInstance(__uuidof(BackgroundCopyManager), nullptr, |
| CLSCTX_ALL, IID_PPV_ARGS(&local_bits_manager)); |
| } |
| |
| if (FAILED(hr)) { |
| return hr; |
| } |
| *bits_manager = local_bits_manager; |
| return S_OK; |
| } |
| |
| void CleanupJob(const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job) { |
| if (!job) { |
| return; |
| } |
| |
| // Get the file paths associated with this job before canceling the job. |
| // Canceling the job removes it from the BITS queue right away. It appears |
| // that it is still possible to query for the properties of the job after |
| // the job has been canceled. It seems safer though to get the paths first. |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyFile>> files; |
| GetFilesInJob(job, &files); |
| |
| std::vector<base::FilePath> paths; |
| for (const auto& file : files) { |
| std::wstring local_name; |
| HRESULT hr = GetJobFileProperties(file, &local_name, nullptr, nullptr); |
| if (SUCCEEDED(hr)) { |
| paths.push_back(base::FilePath(local_name)); |
| } |
| } |
| |
| job->Cancel(); |
| |
| for (const auto& path : paths) { |
| DeleteFileAndEmptyParentDirectory(path); |
| } |
| } |
| |
| void CheckIsMta() { |
| CHECK_EQ(base::win::GetComApartmentTypeForThread(), |
| base::win::ComApartmentType::MTA); |
| } |
| |
| } // namespace |
| |
| BackgroundDownloader::BackgroundDownloader( |
| scoped_refptr<CrxDownloader> successor) |
| : CrxDownloader(std::move(successor)), |
| com_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| kTaskTraitsBackgroundDownloader)) { |
| DETACH_FROM_SEQUENCE(com_sequence_checker_); |
| } |
| |
| BackgroundDownloader::~BackgroundDownloader() { |
| DETACH_FROM_SEQUENCE(com_sequence_checker_); |
| } |
| |
| void BackgroundDownloader::StartTimer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| timer_ = std::make_unique<base::OneShotTimer>(); |
| timer_->Start(FROM_HERE, base::Seconds(kJobPollingIntervalSec), this, |
| &BackgroundDownloader::OnTimer); |
| } |
| |
| void BackgroundDownloader::OnTimer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| timer_ = nullptr; |
| com_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&BackgroundDownloader::OnDownloading, this)); |
| } |
| |
| base::OnceClosure BackgroundDownloader::DoStartDownload(const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| com_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&BackgroundDownloader::BeginDownload, this, url)); |
| return base::DoNothing(); |
| } |
| |
| // Called one time when this class is asked to do a download. |
| void BackgroundDownloader::BeginDownload(const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| download_start_time_ = base::TimeTicks::Now(); |
| job_stuck_begin_time_ = download_start_time_; |
| |
| HRESULT hr = BeginDownloadHelper(url); |
| if (FAILED(hr)) { |
| EndDownload(hr); |
| return; |
| } |
| |
| VLOG(1) << "Starting BITS download for: " << url.spec(); |
| |
| main_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&BackgroundDownloader::StartTimer, this)); |
| } |
| |
| // Creates or opens an existing BITS job to download the |url|, and handles |
| // the marshalling of the interfaces in GIT. |
| HRESULT BackgroundDownloader::BeginDownloadHelper(const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| HRESULT hr = CreateBitsManager(&bits_manager_); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| hr = QueueBitsJob(url, &job_); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| return S_OK; |
| } |
| |
| // Called any time the timer fires. |
| void BackgroundDownloader::OnDownloading() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| BG_JOB_STATE job_state = BG_JOB_STATE_ERROR; |
| HRESULT hr = job_->GetState(&job_state); |
| if (FAILED(hr)) { |
| EndDownload(hr); |
| return; |
| } |
| |
| bool is_handled = false; |
| switch (job_state) { |
| case BG_JOB_STATE_TRANSFERRED: |
| is_handled = OnStateTransferred(); |
| break; |
| |
| case BG_JOB_STATE_ERROR: |
| is_handled = OnStateError(); |
| break; |
| |
| case BG_JOB_STATE_CANCELLED: |
| is_handled = OnStateCancelled(); |
| break; |
| |
| case BG_JOB_STATE_ACKNOWLEDGED: |
| is_handled = OnStateAcknowledged(); |
| break; |
| |
| case BG_JOB_STATE_QUEUED: |
| // Fall through. |
| case BG_JOB_STATE_CONNECTING: |
| // Fall through. |
| case BG_JOB_STATE_SUSPENDED: |
| is_handled = OnStateQueued(); |
| break; |
| |
| case BG_JOB_STATE_TRANSIENT_ERROR: |
| is_handled = OnStateTransientError(); |
| break; |
| |
| case BG_JOB_STATE_TRANSFERRING: |
| is_handled = OnStateTransferring(); |
| break; |
| |
| default: |
| break; |
| } |
| |
| if (is_handled) { |
| return; |
| } |
| |
| main_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&BackgroundDownloader::StartTimer, this)); |
| } |
| |
| // Completes the BITS download, picks up the file path of the response, and |
| // notifies the CrxDownloader. The function should be called only once. |
| void BackgroundDownloader::EndDownload(HRESULT error) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| const base::TimeTicks download_end_time(base::TimeTicks::Now()); |
| const base::TimeDelta download_time = |
| download_end_time >= download_start_time_ |
| ? download_end_time - download_start_time_ |
| : base::TimeDelta(); |
| |
| int64_t downloaded_bytes = -1; |
| int64_t total_bytes = -1; |
| GetJobByteCount(job_, &downloaded_bytes, &total_bytes); |
| |
| if (FAILED(error)) { |
| CleanupJob(job_); |
| } |
| |
| job_ = nullptr; |
| bits_manager_ = nullptr; |
| |
| // Consider the url handled if it has been successfully downloaded or a |
| // 5xx has been received. |
| const bool is_handled = |
| SUCCEEDED(error) || IsHttpServerError(GetHttpStatusFromBitsError(error)); |
| |
| const int error_to_report = SUCCEEDED(error) ? 0 : error; |
| |
| DownloadMetrics download_metrics; |
| download_metrics.url = url(); |
| download_metrics.downloader = DownloadMetrics::kBits; |
| download_metrics.error = error_to_report; |
| download_metrics.downloaded_bytes = downloaded_bytes; |
| download_metrics.total_bytes = total_bytes; |
| download_metrics.download_time_ms = download_time.InMilliseconds(); |
| |
| Result result; |
| result.error = error_to_report; |
| if (!result.error) { |
| result.response = response_; |
| } |
| main_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&BackgroundDownloader::OnDownloadComplete, this, |
| is_handled, result, download_metrics)); |
| } |
| |
| // Called when the BITS job has been transferred successfully. Completes the |
| // BITS job by removing it from the BITS queue and making the download |
| // available to the caller. |
| bool BackgroundDownloader::OnStateTransferred() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| EndDownload(CompleteJob()); |
| return true; |
| } |
| |
| // Called when the job has encountered an error and no further progress can |
| // be made. Cancels this job and removes it from the BITS queue. |
| bool BackgroundDownloader::OnStateError() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| HRESULT error_code = S_OK; |
| HRESULT hr = GetJobError(job_, &error_code); |
| if (FAILED(hr)) { |
| error_code = hr; |
| } |
| CHECK(FAILED(error_code)); |
| EndDownload(error_code); |
| return true; |
| } |
| |
| // Called when the download was completed. This notification is not seen |
| // in the current implementation but provided here as a defensive programming |
| // measure. |
| bool BackgroundDownloader::OnStateAcknowledged() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| EndDownload(E_UNEXPECTED); |
| return true; |
| } |
| |
| // Called when the download was cancelled. Same as above. |
| bool BackgroundDownloader::OnStateCancelled() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| EndDownload(E_UNEXPECTED); |
| return true; |
| } |
| |
| // Called when the job has encountered a transient error, such as a |
| // network disconnect, a server error, or some other recoverable error. |
| bool BackgroundDownloader::OnStateTransientError() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| // Don't retry at all if the transient error was a 5xx. |
| HRESULT error_code = S_OK; |
| HRESULT hr = GetJobError(job_, &error_code); |
| if (SUCCEEDED(hr) && |
| IsHttpServerError(GetHttpStatusFromBitsError(error_code))) { |
| return OnStateError(); |
| } |
| |
| return false; |
| } |
| |
| bool BackgroundDownloader::OnStateQueued() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| if (!IsStuck()) { |
| return false; |
| } |
| |
| // Terminate the download if the job has not made progress in a while. |
| EndDownload(E_ABORT); |
| return true; |
| } |
| |
| bool BackgroundDownloader::OnStateTransferring() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| // Resets the baseline for detecting a stuck job since the job is transferring |
| // data and it is making progress. |
| job_stuck_begin_time_ = base::TimeTicks::Now(); |
| |
| int64_t downloaded_bytes = -1; |
| int64_t total_bytes = -1; |
| GetJobByteCount(job_, &downloaded_bytes, &total_bytes); |
| |
| main_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&BackgroundDownloader::OnDownloadProgress, this, |
| downloaded_bytes, total_bytes)); |
| return false; |
| } |
| |
| HRESULT BackgroundDownloader::QueueBitsJob( |
| const GURL& url, |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob>* job) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| // Remove some old jobs from the BITS queue before creating new jobs. |
| CleanupStaleJobs(); |
| |
| size_t num_jobs = std::numeric_limits<size_t>::max(); |
| HRESULT hr = GetBackgroundDownloaderJobCount(&num_jobs); |
| |
| if (FAILED(hr) || num_jobs >= kMaxQueuedJobs) { |
| return MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, |
| CrxDownloaderError::BITS_TOO_MANY_JOBS); |
| } |
| |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob> local_job; |
| hr = CreateOrOpenJob(url, &local_job); |
| if (FAILED(hr)) { |
| CleanupJob(local_job); |
| return hr; |
| } |
| |
| hr = local_job->Resume(); |
| if (FAILED(hr)) { |
| CleanupJob(local_job); |
| return hr; |
| } |
| |
| *job = local_job; |
| return S_OK; |
| } |
| |
| HRESULT BackgroundDownloader::CreateOrOpenJob( |
| const GURL& url, |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob>* job) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyJob>> jobs; |
| HRESULT hr = FindBitsJobIf( |
| [&url](Microsoft::WRL::ComPtr<IBackgroundCopyJob> job) { |
| return JobFileUrlEqualPredicate(job, url); |
| }, |
| bits_manager_, &jobs); |
| if (SUCCEEDED(hr) && !jobs.empty()) { |
| *job = jobs.front(); |
| return S_FALSE; |
| } |
| |
| Microsoft::WRL::ComPtr<IBackgroundCopyJob> local_job; |
| |
| GUID guid = {0}; |
| hr = bits_manager_->CreateJob(kJobName, BG_JOB_TYPE_DOWNLOAD, &guid, |
| &local_job); |
| if (FAILED(hr)) { |
| CleanupJob(local_job); |
| return hr; |
| } |
| |
| hr = InitializeNewJob(local_job, url); |
| if (FAILED(hr)) { |
| CleanupJob(local_job); |
| return hr; |
| } |
| |
| *job = local_job; |
| return S_OK; |
| } |
| |
| HRESULT BackgroundDownloader::InitializeNewJob( |
| const Microsoft::WRL::ComPtr<IBackgroundCopyJob>& job, |
| const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| base::FilePath tempdir; |
| if (!base::CreateNewTempDirectory(kDownloadDirectoryPrefix, &tempdir)) { |
| return E_FAIL; |
| } |
| |
| const std::wstring filename(base::SysUTF8ToWide(url.ExtractFileName())); |
| HRESULT hr = job->AddFile(base::SysUTF8ToWide(url.spec()).c_str(), |
| tempdir.Append(filename).value().c_str()); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| hr = job->SetDescription(filename.c_str()); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| hr = job->SetPriority(BG_JOB_PRIORITY_NORMAL); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| hr = job->SetMinimumRetryDelay(60 * kMinimumRetryDelayMin); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| const int kSecondsDay = 60 * 60 * 24; |
| hr = job->SetNoProgressTimeout(kSecondsDay * kSetNoProgressTimeoutDays); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| return S_OK; |
| } |
| |
| bool BackgroundDownloader::IsStuck() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| const base::TimeDelta job_stuck_timeout(base::Minutes(kJobStuckTimeoutMin)); |
| return job_stuck_begin_time_ + job_stuck_timeout < base::TimeTicks::Now(); |
| } |
| |
| HRESULT BackgroundDownloader::CompleteJob() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| |
| HRESULT hr = job_->Complete(); |
| if (FAILED(hr) && hr != BG_S_UNABLE_TO_DELETE_FILES) { |
| return hr; |
| } |
| |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyFile>> files; |
| hr = GetFilesInJob(job_, &files); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| if (files.empty()) { |
| return E_UNEXPECTED; |
| } |
| |
| std::wstring local_name; |
| BG_FILE_PROGRESS progress = {0}; |
| hr = GetJobFileProperties(files.front(), &local_name, nullptr, &progress); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| // Check the post-conditions of a successful download, including the file and |
| // job invariants. The byte counts for a job and its file must match as a job |
| // only contains one file. |
| CHECK(progress.Completed); |
| CHECK_EQ(progress.BytesTotal, progress.BytesTransferred); |
| |
| response_ = base::FilePath(local_name); |
| |
| return S_OK; |
| } |
| |
| HRESULT BackgroundDownloader::GetBackgroundDownloaderJobCount( |
| size_t* num_jobs) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| CHECK(bits_manager_); |
| |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyJob>> jobs; |
| const HRESULT hr = FindBitsJobIf( |
| [](const Microsoft::WRL::ComPtr<IBackgroundCopyJob>&) { return true; }, |
| bits_manager_, &jobs); |
| if (FAILED(hr)) { |
| return hr; |
| } |
| |
| *num_jobs = jobs.size(); |
| return S_OK; |
| } |
| |
| void BackgroundDownloader::CleanupStaleJobs() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| CheckIsMta(); |
| CHECK(bits_manager_); |
| |
| static base::Time last_sweep; |
| |
| const base::TimeDelta time_delta( |
| base::Days(kPurgeStaleJobsIntervalBetweenChecksDays)); |
| const base::Time current_time(base::Time::Now()); |
| if (last_sweep + time_delta > current_time) { |
| return; |
| } |
| |
| last_sweep = current_time; |
| |
| std::vector<Microsoft::WRL::ComPtr<IBackgroundCopyJob>> jobs; |
| FindBitsJobIf( |
| [](Microsoft::WRL::ComPtr<IBackgroundCopyJob> job) { |
| return JobCreationOlderThanDaysPredicate(job, kPurgeStaleJobsAfterDays); |
| }, |
| bits_manager_, &jobs); |
| |
| for (const auto& job : jobs) { |
| CleanupJob(job); |
| } |
| |
| CleanupStaleDownloads(); |
| } |
| |
| void BackgroundDownloader::CleanupStaleDownloads() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| EnumerateDownloadDirs( |
| kDownloadDirectoryPrefixMatcher, [](const base::FilePath& dir) { |
| const base::Time now = base::Time::Now(); |
| base::File::Info info; |
| if (base::GetFileInfo(dir, &info) && |
| info.creation_time + base::Days(kPurgeStaleJobsAfterDays) < now) { |
| RetryDeletePathRecursively(dir); |
| } |
| }); |
| } |
| |
| void BackgroundDownloader::EnumerateDownloadDirs( |
| const base::FilePath::StringType& matcher, |
| base::FunctionRef<void(const base::FilePath& dir)> callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(com_sequence_checker_); |
| base::FilePath dir; |
| std::vector<base::FilePath> dirs; |
| if (base::PathService::Get(base::DIR_SYSTEM_TEMP, &dir)) { |
| dirs.push_back(dir); |
| } |
| if (base::GetTempDir(&dir)) { |
| dirs.push_back(dir); |
| } |
| std::ranges::for_each(dirs, [&](const base::FilePath& parent_dir) { |
| base::FileEnumerator(parent_dir, |
| /*recursive=*/false, base::FileEnumerator::DIRECTORIES, |
| matcher) |
| .ForEach(callback); |
| }); |
| } |
| |
| } // namespace update_client |