| 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/update_client/background_downloader_win.h" | 5 #include "components/update_client/background_downloader_win.h" |
| 6 | 6 |
| 7 #include <atlbase.h> | 7 #include <atlbase.h> |
| 8 #include <atlcom.h> | 8 #include <atlcom.h> |
| 9 #include <stddef.h> | 9 #include <stddef.h> |
| 10 | 10 |
| (...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 141 DWORD cookie, | 141 DWORD cookie, |
| 142 ScopedComPtr<T>* p) { | 142 ScopedComPtr<T>* p) { |
| 143 return git->GetInterfaceFromGlobal(cookie, __uuidof(T), p->ReceiveVoid()); | 143 return git->GetInterfaceFromGlobal(cookie, __uuidof(T), p->ReceiveVoid()); |
| 144 } | 144 } |
| 145 | 145 |
| 146 // Registers an interface pointer in GIT and returns its corresponding |cookie|. | 146 // Registers an interface pointer in GIT and returns its corresponding |cookie|. |
| 147 template <typename T> | 147 template <typename T> |
| 148 HRESULT RegisterInterfaceInGit(const ScopedComPtr<IGlobalInterfaceTable>& git, | 148 HRESULT RegisterInterfaceInGit(const ScopedComPtr<IGlobalInterfaceTable>& git, |
| 149 const ScopedComPtr<T>& p, | 149 const ScopedComPtr<T>& p, |
| 150 DWORD* cookie) { | 150 DWORD* cookie) { |
| 151 return git->RegisterInterfaceInGlobal(p.get(), __uuidof(T), cookie); | 151 return git->RegisterInterfaceInGlobal(p.Get(), __uuidof(T), cookie); |
| 152 } | 152 } |
| 153 | 153 |
| 154 // Returns the status code from a given BITS error. | 154 // Returns the status code from a given BITS error. |
| 155 int GetHttpStatusFromBitsError(HRESULT error) { | 155 int GetHttpStatusFromBitsError(HRESULT error) { |
| 156 // BITS errors are defined in bitsmsg.h. Although not documented, it is | 156 // BITS errors are defined in bitsmsg.h. Although not documented, it is |
| 157 // clear that all errors corresponding to http status code have the high | 157 // clear that all errors corresponding to http status code have the high |
| 158 // word equal to 0x8019 and the low word equal to the http status code. | 158 // word equal to 0x8019 and the low word equal to the http status code. |
| 159 const int kHttpStatusFirst = 100; // Continue. | 159 const int kHttpStatusFirst = 100; // Continue. |
| 160 const int kHttpStatusLast = 505; // Version not supported. | 160 const int kHttpStatusLast = 505; // Version not supported. |
| 161 bool is_valid = HIWORD(error) == 0x8019 && | 161 bool is_valid = HIWORD(error) == 0x8019 && |
| (...skipping 10 matching lines...) Expand all Loading... |
| 172 if (FAILED(hr)) | 172 if (FAILED(hr)) |
| 173 return hr; | 173 return hr; |
| 174 | 174 |
| 175 ULONG num_files = 0; | 175 ULONG num_files = 0; |
| 176 hr = enum_files->GetCount(&num_files); | 176 hr = enum_files->GetCount(&num_files); |
| 177 if (FAILED(hr)) | 177 if (FAILED(hr)) |
| 178 return hr; | 178 return hr; |
| 179 | 179 |
| 180 for (ULONG i = 0; i != num_files; ++i) { | 180 for (ULONG i = 0; i != num_files; ++i) { |
| 181 ScopedComPtr<IBackgroundCopyFile> file; | 181 ScopedComPtr<IBackgroundCopyFile> file; |
| 182 if (enum_files->Next(1, file.Receive(), NULL) == S_OK && file.get()) | 182 if (enum_files->Next(1, file.Receive(), NULL) == S_OK && file.Get()) |
| 183 files->push_back(file); | 183 files->push_back(file); |
| 184 } | 184 } |
| 185 | 185 |
| 186 return S_OK; | 186 return S_OK; |
| 187 } | 187 } |
| 188 | 188 |
| 189 // Returns the file name, the url, and some per-file progress information. | 189 // Returns the file name, the url, and some per-file progress information. |
| 190 // The function out parameters can be NULL if that data is not requested. | 190 // The function out parameters can be NULL if that data is not requested. |
| 191 HRESULT GetJobFileProperties(IBackgroundCopyFile* file, | 191 HRESULT GetJobFileProperties(IBackgroundCopyFile* file, |
| 192 base::string16* local_name, | 192 base::string16* local_name, |
| (...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 293 ULONG job_count = 0; | 293 ULONG job_count = 0; |
| 294 hr = enum_jobs->GetCount(&job_count); | 294 hr = enum_jobs->GetCount(&job_count); |
| 295 if (FAILED(hr)) | 295 if (FAILED(hr)) |
| 296 return hr; | 296 return hr; |
| 297 | 297 |
| 298 // Iterate over jobs, run the predicate, and select the job only if | 298 // Iterate over jobs, run the predicate, and select the job only if |
| 299 // the job description matches the component updater jobs. | 299 // the job description matches the component updater jobs. |
| 300 for (ULONG i = 0; i != job_count; ++i) { | 300 for (ULONG i = 0; i != job_count; ++i) { |
| 301 ScopedComPtr<IBackgroundCopyJob> current_job; | 301 ScopedComPtr<IBackgroundCopyJob> current_job; |
| 302 if (enum_jobs->Next(1, current_job.Receive(), NULL) == S_OK && | 302 if (enum_jobs->Next(1, current_job.Receive(), NULL) == S_OK && |
| 303 pred(current_job.get())) { | 303 pred(current_job.Get())) { |
| 304 base::string16 job_description; | 304 base::string16 job_description; |
| 305 hr = GetJobDescription(current_job.get(), &job_description); | 305 hr = GetJobDescription(current_job.Get(), &job_description); |
| 306 if (job_description.compare(kJobDescription) == 0) | 306 if (job_description.compare(kJobDescription) == 0) |
| 307 jobs->push_back(current_job); | 307 jobs->push_back(current_job); |
| 308 } | 308 } |
| 309 } | 309 } |
| 310 | 310 |
| 311 return jobs->empty() ? S_FALSE : S_OK; | 311 return jobs->empty() ? S_FALSE : S_OK; |
| 312 } | 312 } |
| 313 | 313 |
| 314 // Compares the job creation time and returns true if the job creation time | 314 // Compares the job creation time and returns true if the job creation time |
| 315 // is older than |num_days|. | 315 // is older than |num_days|. |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 372 *bits_manager = object.Detach(); | 372 *bits_manager = object.Detach(); |
| 373 return S_OK; | 373 return S_OK; |
| 374 } | 374 } |
| 375 | 375 |
| 376 void CleanupJobFiles(IBackgroundCopyJob* job) { | 376 void CleanupJobFiles(IBackgroundCopyJob* job) { |
| 377 std::vector<ScopedComPtr<IBackgroundCopyFile>> files; | 377 std::vector<ScopedComPtr<IBackgroundCopyFile>> files; |
| 378 if (FAILED(GetFilesInJob(job, &files))) | 378 if (FAILED(GetFilesInJob(job, &files))) |
| 379 return; | 379 return; |
| 380 for (size_t i = 0; i != files.size(); ++i) { | 380 for (size_t i = 0; i != files.size(); ++i) { |
| 381 base::string16 local_name; | 381 base::string16 local_name; |
| 382 HRESULT hr(GetJobFileProperties(files[i].get(), &local_name, NULL, NULL)); | 382 HRESULT hr(GetJobFileProperties(files[i].Get(), &local_name, NULL, NULL)); |
| 383 if (SUCCEEDED(hr)) | 383 if (SUCCEEDED(hr)) |
| 384 DeleteFileAndEmptyParentDirectory(base::FilePath(local_name)); | 384 DeleteFileAndEmptyParentDirectory(base::FilePath(local_name)); |
| 385 } | 385 } |
| 386 } | 386 } |
| 387 | 387 |
| 388 // Cleans up incompleted jobs that are too old. | 388 // Cleans up incompleted jobs that are too old. |
| 389 HRESULT CleanupStaleJobs( | 389 HRESULT CleanupStaleJobs( |
| 390 const ScopedComPtr<IBackgroundCopyManager>& bits_manager) { | 390 const ScopedComPtr<IBackgroundCopyManager>& bits_manager) { |
| 391 if (!bits_manager.get()) | 391 if (!bits_manager.Get()) |
| 392 return E_FAIL; | 392 return E_FAIL; |
| 393 | 393 |
| 394 static base::Time last_sweep; | 394 static base::Time last_sweep; |
| 395 | 395 |
| 396 const base::TimeDelta time_delta( | 396 const base::TimeDelta time_delta( |
| 397 base::TimeDelta::FromDays(kPurgeStaleJobsIntervalBetweenChecksDays)); | 397 base::TimeDelta::FromDays(kPurgeStaleJobsIntervalBetweenChecksDays)); |
| 398 const base::Time current_time(base::Time::Now()); | 398 const base::Time current_time(base::Time::Now()); |
| 399 if (last_sweep + time_delta > current_time) | 399 if (last_sweep + time_delta > current_time) |
| 400 return S_OK; | 400 return S_OK; |
| 401 | 401 |
| 402 last_sweep = current_time; | 402 last_sweep = current_time; |
| 403 | 403 |
| 404 std::vector<ScopedComPtr<IBackgroundCopyJob>> jobs; | 404 std::vector<ScopedComPtr<IBackgroundCopyJob>> jobs; |
| 405 HRESULT hr = FindBitsJobIf( | 405 HRESULT hr = FindBitsJobIf( |
| 406 JobCreationOlderThanDays(kPurgeStaleJobsAfterDays), | 406 JobCreationOlderThanDays(kPurgeStaleJobsAfterDays), |
| 407 bits_manager.get(), &jobs); | 407 bits_manager.Get(), &jobs); |
| 408 if (FAILED(hr)) | 408 if (FAILED(hr)) |
| 409 return hr; | 409 return hr; |
| 410 | 410 |
| 411 for (size_t i = 0; i != jobs.size(); ++i) { | 411 for (size_t i = 0; i != jobs.size(); ++i) { |
| 412 jobs[i]->Cancel(); | 412 jobs[i]->Cancel(); |
| 413 CleanupJobFiles(jobs[i].get()); | 413 CleanupJobFiles(jobs[i].Get()); |
| 414 } | 414 } |
| 415 | 415 |
| 416 return S_OK; | 416 return S_OK; |
| 417 } | 417 } |
| 418 | 418 |
| 419 } // namespace | 419 } // namespace |
| 420 | 420 |
| 421 BackgroundDownloader::BackgroundDownloader( | 421 BackgroundDownloader::BackgroundDownloader( |
| 422 std::unique_ptr<CrxDownloader> successor, | 422 std::unique_ptr<CrxDownloader> successor, |
| 423 net::URLRequestContextGetter* context_getter, | 423 net::URLRequestContextGetter* context_getter, |
| (...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 574 DCHECK(!TimerIsRunning()); | 574 DCHECK(!TimerIsRunning()); |
| 575 | 575 |
| 576 const base::TimeTicks download_end_time(base::TimeTicks::Now()); | 576 const base::TimeTicks download_end_time(base::TimeTicks::Now()); |
| 577 const base::TimeDelta download_time = | 577 const base::TimeDelta download_time = |
| 578 download_end_time >= download_start_time_ | 578 download_end_time >= download_start_time_ |
| 579 ? download_end_time - download_start_time_ | 579 ? download_end_time - download_start_time_ |
| 580 : base::TimeDelta(); | 580 : base::TimeDelta(); |
| 581 | 581 |
| 582 int64_t downloaded_bytes = -1; | 582 int64_t downloaded_bytes = -1; |
| 583 int64_t total_bytes = -1; | 583 int64_t total_bytes = -1; |
| 584 GetJobByteCount(job_.get(), &downloaded_bytes, &total_bytes); | 584 GetJobByteCount(job_.Get(), &downloaded_bytes, &total_bytes); |
| 585 | 585 |
| 586 if (FAILED(error) && job_.get()) { | 586 if (FAILED(error) && job_.Get()) { |
| 587 job_->Cancel(); | 587 job_->Cancel(); |
| 588 CleanupJobFiles(job_.get()); | 588 CleanupJobFiles(job_.Get()); |
| 589 } | 589 } |
| 590 | 590 |
| 591 CleanupStaleJobs(bits_manager_); | 591 CleanupStaleJobs(bits_manager_); |
| 592 | 592 |
| 593 ClearGit(); | 593 ClearGit(); |
| 594 | 594 |
| 595 // Consider the url handled if it has been successfully downloaded or a | 595 // Consider the url handled if it has been successfully downloaded or a |
| 596 // 5xx has been received. | 596 // 5xx has been received. |
| 597 const bool is_handled = | 597 const bool is_handled = |
| 598 SUCCEEDED(error) || IsHttpServerError(GetHttpStatusFromBitsError(error)); | 598 SUCCEEDED(error) || IsHttpServerError(GetHttpStatusFromBitsError(error)); |
| (...skipping 29 matching lines...) Expand all Loading... |
| 628 // available to the caller. | 628 // available to the caller. |
| 629 bool BackgroundDownloader::OnStateTransferred() { | 629 bool BackgroundDownloader::OnStateTransferred() { |
| 630 EndDownload(CompleteJob()); | 630 EndDownload(CompleteJob()); |
| 631 return true; | 631 return true; |
| 632 } | 632 } |
| 633 | 633 |
| 634 // Called when the job has encountered an error and no further progress can | 634 // Called when the job has encountered an error and no further progress can |
| 635 // be made. Cancels this job and removes it from the BITS queue. | 635 // be made. Cancels this job and removes it from the BITS queue. |
| 636 bool BackgroundDownloader::OnStateError() { | 636 bool BackgroundDownloader::OnStateError() { |
| 637 HRESULT error_code = S_OK; | 637 HRESULT error_code = S_OK; |
| 638 HRESULT hr = GetJobError(job_.get(), &error_code); | 638 HRESULT hr = GetJobError(job_.Get(), &error_code); |
| 639 if (FAILED(hr)) | 639 if (FAILED(hr)) |
| 640 error_code = hr; | 640 error_code = hr; |
| 641 | 641 |
| 642 DCHECK(FAILED(error_code)); | 642 DCHECK(FAILED(error_code)); |
| 643 EndDownload(error_code); | 643 EndDownload(error_code); |
| 644 return true; | 644 return true; |
| 645 } | 645 } |
| 646 | 646 |
| 647 // Called when the download was completed. This notification is not seen | 647 // Called when the download was completed. This notification is not seen |
| 648 // in the current implementation but provided here as a defensive programming | 648 // in the current implementation but provided here as a defensive programming |
| (...skipping 14 matching lines...) Expand all Loading... |
| 663 bool BackgroundDownloader::OnStateTransientError() { | 663 bool BackgroundDownloader::OnStateTransientError() { |
| 664 // If the job appears to be stuck, handle the transient error as if | 664 // If the job appears to be stuck, handle the transient error as if |
| 665 // it were a final error. This causes the job to be cancelled and a specific | 665 // it were a final error. This causes the job to be cancelled and a specific |
| 666 // error be returned, if the error was available. | 666 // error be returned, if the error was available. |
| 667 if (IsStuck()) { | 667 if (IsStuck()) { |
| 668 return OnStateError(); | 668 return OnStateError(); |
| 669 } | 669 } |
| 670 | 670 |
| 671 // Don't retry at all if the transient error was a 5xx. | 671 // Don't retry at all if the transient error was a 5xx. |
| 672 HRESULT error_code = S_OK; | 672 HRESULT error_code = S_OK; |
| 673 HRESULT hr = GetJobError(job_.get(), &error_code); | 673 HRESULT hr = GetJobError(job_.Get(), &error_code); |
| 674 if (SUCCEEDED(hr) && | 674 if (SUCCEEDED(hr) && |
| 675 IsHttpServerError(GetHttpStatusFromBitsError(error_code))) { | 675 IsHttpServerError(GetHttpStatusFromBitsError(error_code))) { |
| 676 return OnStateError(); | 676 return OnStateError(); |
| 677 } | 677 } |
| 678 | 678 |
| 679 return false; | 679 return false; |
| 680 } | 680 } |
| 681 | 681 |
| 682 bool BackgroundDownloader::OnStateQueued() { | 682 bool BackgroundDownloader::OnStateQueued() { |
| 683 if (!IsStuck()) | 683 if (!IsStuck()) |
| 684 return false; | 684 return false; |
| 685 | 685 |
| 686 // Terminate the download if the job has not made progress in a while. | 686 // Terminate the download if the job has not made progress in a while. |
| 687 EndDownload(E_ABORT); | 687 EndDownload(E_ABORT); |
| 688 return true; | 688 return true; |
| 689 } | 689 } |
| 690 | 690 |
| 691 bool BackgroundDownloader::OnStateTransferring() { | 691 bool BackgroundDownloader::OnStateTransferring() { |
| 692 // Resets the baseline for detecting a stuck job since the job is transferring | 692 // Resets the baseline for detecting a stuck job since the job is transferring |
| 693 // data and it is making progress. | 693 // data and it is making progress. |
| 694 job_stuck_begin_time_ = base::TimeTicks::Now(); | 694 job_stuck_begin_time_ = base::TimeTicks::Now(); |
| 695 | 695 |
| 696 int64_t downloaded_bytes = -1; | 696 int64_t downloaded_bytes = -1; |
| 697 int64_t total_bytes = -1; | 697 int64_t total_bytes = -1; |
| 698 HRESULT hr = GetJobByteCount(job_.get(), &downloaded_bytes, &total_bytes); | 698 HRESULT hr = GetJobByteCount(job_.Get(), &downloaded_bytes, &total_bytes); |
| 699 if (FAILED(hr)) | 699 if (FAILED(hr)) |
| 700 return false; | 700 return false; |
| 701 | 701 |
| 702 Result result; | 702 Result result; |
| 703 result.downloaded_bytes = downloaded_bytes; | 703 result.downloaded_bytes = downloaded_bytes; |
| 704 result.total_bytes = total_bytes; | 704 result.total_bytes = total_bytes; |
| 705 | 705 |
| 706 main_task_runner()->PostTask( | 706 main_task_runner()->PostTask( |
| 707 FROM_HERE, base::Bind(&BackgroundDownloader::OnDownloadProgress, | 707 FROM_HERE, base::Bind(&BackgroundDownloader::OnDownloadProgress, |
| 708 base::Unretained(this), result)); | 708 base::Unretained(this), result)); |
| (...skipping 25 matching lines...) Expand all Loading... |
| 734 *job = p.Detach(); | 734 *job = p.Detach(); |
| 735 | 735 |
| 736 return S_OK; | 736 return S_OK; |
| 737 } | 737 } |
| 738 | 738 |
| 739 HRESULT BackgroundDownloader::CreateOrOpenJob(const GURL& url, | 739 HRESULT BackgroundDownloader::CreateOrOpenJob(const GURL& url, |
| 740 IBackgroundCopyJob** job) { | 740 IBackgroundCopyJob** job) { |
| 741 std::vector<ScopedComPtr<IBackgroundCopyJob>> jobs; | 741 std::vector<ScopedComPtr<IBackgroundCopyJob>> jobs; |
| 742 HRESULT hr = FindBitsJobIf( | 742 HRESULT hr = FindBitsJobIf( |
| 743 JobFileUrlEqual(base::SysUTF8ToWide(url.spec())), | 743 JobFileUrlEqual(base::SysUTF8ToWide(url.spec())), |
| 744 bits_manager_.get(), &jobs); | 744 bits_manager_.Get(), &jobs); |
| 745 if (SUCCEEDED(hr) && !jobs.empty()) { | 745 if (SUCCEEDED(hr) && !jobs.empty()) { |
| 746 *job = jobs.front().Detach(); | 746 *job = jobs.front().Detach(); |
| 747 return S_FALSE; | 747 return S_FALSE; |
| 748 } | 748 } |
| 749 | 749 |
| 750 // Use kJobDescription as a temporary job display name until the proper | 750 // Use kJobDescription as a temporary job display name until the proper |
| 751 // display name is initialized later on. | 751 // display name is initialized later on. |
| 752 GUID guid = {0}; | 752 GUID guid = {0}; |
| 753 hr = bits_manager_->CreateJob(kJobDescription, BG_JOB_TYPE_DOWNLOAD, &guid, | 753 hr = bits_manager_->CreateJob(kJobDescription, BG_JOB_TYPE_DOWNLOAD, &guid, |
| 754 job); | 754 job); |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 802 base::TimeDelta::FromMinutes(kJobStuckTimeoutMin)); | 802 base::TimeDelta::FromMinutes(kJobStuckTimeoutMin)); |
| 803 return job_stuck_begin_time_ + job_stuck_timeout < base::TimeTicks::Now(); | 803 return job_stuck_begin_time_ + job_stuck_timeout < base::TimeTicks::Now(); |
| 804 } | 804 } |
| 805 | 805 |
| 806 HRESULT BackgroundDownloader::CompleteJob() { | 806 HRESULT BackgroundDownloader::CompleteJob() { |
| 807 HRESULT hr = job_->Complete(); | 807 HRESULT hr = job_->Complete(); |
| 808 if (FAILED(hr) && hr != BG_S_UNABLE_TO_DELETE_FILES) | 808 if (FAILED(hr) && hr != BG_S_UNABLE_TO_DELETE_FILES) |
| 809 return hr; | 809 return hr; |
| 810 | 810 |
| 811 std::vector<ScopedComPtr<IBackgroundCopyFile>> files; | 811 std::vector<ScopedComPtr<IBackgroundCopyFile>> files; |
| 812 hr = GetFilesInJob(job_.get(), &files); | 812 hr = GetFilesInJob(job_.Get(), &files); |
| 813 if (FAILED(hr)) | 813 if (FAILED(hr)) |
| 814 return hr; | 814 return hr; |
| 815 | 815 |
| 816 if (files.empty()) | 816 if (files.empty()) |
| 817 return E_UNEXPECTED; | 817 return E_UNEXPECTED; |
| 818 | 818 |
| 819 base::string16 local_name; | 819 base::string16 local_name; |
| 820 BG_FILE_PROGRESS progress = {0}; | 820 BG_FILE_PROGRESS progress = {0}; |
| 821 hr = GetJobFileProperties(files.front().get(), &local_name, NULL, &progress); | 821 hr = GetJobFileProperties(files.front().Get(), &local_name, NULL, &progress); |
| 822 if (FAILED(hr)) | 822 if (FAILED(hr)) |
| 823 return hr; | 823 return hr; |
| 824 | 824 |
| 825 // Sanity check the post-conditions of a successful download, including | 825 // Sanity check the post-conditions of a successful download, including |
| 826 // the file and job invariants. The byte counts for a job and its file | 826 // the file and job invariants. The byte counts for a job and its file |
| 827 // must match as a job only contains one file. | 827 // must match as a job only contains one file. |
| 828 DCHECK(progress.Completed); | 828 DCHECK(progress.Completed); |
| 829 DCHECK_EQ(progress.BytesTotal, progress.BytesTransferred); | 829 DCHECK_EQ(progress.BytesTotal, progress.BytesTransferred); |
| 830 | 830 |
| 831 response_ = base::FilePath(local_name); | 831 response_ = base::FilePath(local_name); |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 875 | 875 |
| 876 for (auto cookie : cookies) { | 876 for (auto cookie : cookies) { |
| 877 // TODO(sorin): check the result of the call, see crbug.com/644857. | 877 // TODO(sorin): check the result of the call, see crbug.com/644857. |
| 878 git->RevokeInterfaceFromGlobal(cookie); | 878 git->RevokeInterfaceFromGlobal(cookie); |
| 879 } | 879 } |
| 880 | 880 |
| 881 return S_OK; | 881 return S_OK; |
| 882 } | 882 } |
| 883 | 883 |
| 884 } // namespace update_client | 884 } // namespace update_client |
| OLD | NEW |