summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorasanka <asanka@chromium.org>2014-09-26 08:43:32 -0700
committerCommit bot <commit-bot@chromium.org>2014-09-26 15:43:49 +0000
commitf4b6a28eb309b1c8ca19458de663188c57839119 (patch)
treeadd3b22bea44ff007001ab6289e88426c6f08141
parent3a65f0bdd05251c15adc454a74bce0c0b83d24aa (diff)
downloadchromium_src-f4b6a28eb309b1c8ca19458de663188c57839119.zip
chromium_src-f4b6a28eb309b1c8ca19458de663188c57839119.tar.gz
chromium_src-f4b6a28eb309b1c8ca19458de663188c57839119.tar.bz2
[Downloads] Retry renames after transient failures.
If the cause of a rename failure is suspected of being transient (sharing violations, suspected race conditions in the file system), then retry the rename with an exponential backoff. BUG=368455 Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=278483 Reverted (Win XP issues): https://chromium.googlesource.com/chromium/src.git/+/d31e3689a5f5a0ae6eba1f15ea299bf84fd6d381 Review URL: https://codereview.chromium.org/319603003 Cr-Commit-Position: refs/heads/master@{#296949}
-rw-r--r--content/browser/download/base_file.cc20
-rw-r--r--content/browser/download/base_file.h14
-rw-r--r--content/browser/download/base_file_posix.cc2
-rw-r--r--content/browser/download/base_file_unittest.cc58
-rw-r--r--content/browser/download/base_file_win.cc41
-rw-r--r--content/browser/download/download_file_impl.cc129
-rw-r--r--content/browser/download/download_file_impl.h29
-rw-r--r--content/browser/download/download_file_unittest.cc339
-rw-r--r--content/browser/download/download_interrupt_reasons_impl.cc29
-rw-r--r--content/browser/download/download_interrupt_reasons_impl.h5
-rw-r--r--content/browser/download/download_stats.cc12
-rw-r--r--content/browser/download/download_stats.h5
-rw-r--r--content/public/browser/download_interrupt_reason_values.h1
-rw-r--r--tools/metrics/histograms/histograms.xml31
14 files changed, 575 insertions, 140 deletions
diff --git a/content/browser/download/base_file.cc b/content/browser/download/base_file.cc
index b715673..5bca783 100644
--- a/content/browser/download/base_file.cc
+++ b/content/browser/download/base_file.cc
@@ -159,15 +159,18 @@ DownloadInterruptReason BaseFile::Rename(const base::FilePath& new_path) {
// permissions / security descriptors that makes sense in the new directory.
rename_result = MoveFileAndAdjustPermissions(new_path);
- if (rename_result == DOWNLOAD_INTERRUPT_REASON_NONE) {
+ if (rename_result == DOWNLOAD_INTERRUPT_REASON_NONE)
full_path_ = new_path;
- // Re-open the file if we were still using it.
- if (was_in_progress)
- rename_result = Open();
- }
+
+ // Re-open the file if we were still using it regardless of the interrupt
+ // reason.
+ DownloadInterruptReason open_result = DOWNLOAD_INTERRUPT_REASON_NONE;
+ if (was_in_progress)
+ open_result = Open();
bound_net_log_.EndEvent(net::NetLog::TYPE_DOWNLOAD_FILE_RENAMED);
- return rename_result;
+ return rename_result == DOWNLOAD_INTERRUPT_REASON_NONE ? open_result
+ : rename_result;
}
void BaseFile::Detach() {
@@ -326,11 +329,10 @@ DownloadInterruptReason BaseFile::LogSystemError(
const char* operation,
logging::SystemErrorCode os_error) {
// There's no direct conversion from a system error to an interrupt reason.
- net::Error net_error = net::MapSystemError(os_error);
+ base::File::Error file_error = base::File::OSErrorToFileError(os_error);
return LogInterruptReason(
operation, os_error,
- ConvertNetErrorToInterruptReason(
- net_error, DOWNLOAD_INTERRUPT_FROM_DISK));
+ ConvertFileErrorToInterruptReason(file_error));
}
DownloadInterruptReason BaseFile::LogInterruptReason(
diff --git a/content/browser/download/base_file.h b/content/browser/download/base_file.h
index a14c5da..037aecc 100644
--- a/content/browser/download/base_file.h
+++ b/content/browser/download/base_file.h
@@ -56,7 +56,10 @@ class CONTENT_EXPORT BaseFile {
DownloadInterruptReason AppendDataToFile(const char* data, size_t data_len);
// Rename the download file. Returns a DownloadInterruptReason indicating the
- // result of the operation.
+ // result of the operation. A return code of NONE indicates that the rename
+ // was successful. After a failure, the full_path() and in_progress() can be
+ // used to determine the last known filename and whether the file is available
+ // for writing or retrying the rename.
virtual DownloadInterruptReason Rename(const base::FilePath& full_path);
// Detach the file so it is not deleted on destruction.
@@ -79,8 +82,15 @@ class CONTENT_EXPORT BaseFile {
// Windows to ensure the correct app client ID is available.
DownloadInterruptReason AnnotateWithSourceInformation();
- base::FilePath full_path() const { return full_path_; }
+ // Returns the last known path to the download file. Can be empty if there's
+ // no file.
+ const base::FilePath& full_path() const { return full_path_; }
+
+ // Returns true if the file is open. If true, the file can be written to or
+ // renamed.
bool in_progress() const { return file_.IsValid(); }
+
+ // Returns the number of bytes in the file pointed to by full_path().
int64 bytes_so_far() const { return bytes_so_far_; }
// Fills |hash| with the hash digest for the file.
diff --git a/content/browser/download/base_file_posix.cc b/content/browser/download/base_file_posix.cc
index 27940be..b5d8e01 100644
--- a/content/browser/download/base_file_posix.cc
+++ b/content/browser/download/base_file_posix.cc
@@ -27,8 +27,6 @@ DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
if (!stat_succeeded)
LogSystemError("stat", errno);
- // TODO(estade): Move() falls back to copying and deleting when a simple
- // rename fails. Copying sucks for large downloads. crbug.com/8737
if (!base::Move(full_path_, new_path))
return LogSystemError("Move", errno);
diff --git a/content/browser/download/base_file_unittest.cc b/content/browser/download/base_file_unittest.cc
index 352f7bb..090e4da 100644
--- a/content/browser/download/base_file_unittest.cc
+++ b/content/browser/download/base_file_unittest.cc
@@ -195,6 +195,12 @@ class BaseFileTest : public testing::Test {
expected_error_ = err;
}
+ void ExpectPermissionError(DownloadInterruptReason err) {
+ EXPECT_TRUE(err == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR ||
+ err == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED)
+ << "Interrupt reason = " << err;
+ }
+
protected:
// BaseClass instance we are testing.
scoped_ptr<BaseFile> base_file_;
@@ -469,13 +475,61 @@ TEST_F(BaseFileTest, RenameWithError) {
{
base::FilePermissionRestorer restore_permissions_for(test_dir);
ASSERT_TRUE(base::MakeFileUnwritable(test_dir));
- EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
- base_file_->Rename(new_path));
+ ExpectPermissionError(base_file_->Rename(new_path));
}
base_file_->Finish();
}
+// Test that if a rename fails for an in-progress BaseFile, it remains writeable
+// and renameable.
+TEST_F(BaseFileTest, RenameWithErrorInProgress) {
+ ASSERT_TRUE(InitializeFile());
+
+ base::FilePath test_dir(temp_dir_.path().AppendASCII("TestDir"));
+ ASSERT_TRUE(base::CreateDirectory(test_dir));
+
+ base::FilePath new_path(test_dir.AppendASCII("TestFile"));
+ EXPECT_FALSE(base::PathExists(new_path));
+
+ // Write some data to start with.
+ ASSERT_TRUE(AppendDataToFile(kTestData1));
+ ASSERT_TRUE(base_file_->in_progress());
+
+ base::FilePath old_path = base_file_->full_path();
+
+ {
+ base::FilePermissionRestorer restore_permissions_for(test_dir);
+ ASSERT_TRUE(base::MakeFileUnwritable(test_dir));
+ ExpectPermissionError(base_file_->Rename(new_path));
+
+ // The file should still be open and we should be able to continue writing
+ // to it.
+ ASSERT_TRUE(base_file_->in_progress());
+ ASSERT_TRUE(AppendDataToFile(kTestData2));
+ ASSERT_EQ(old_path.value(), base_file_->full_path().value());
+
+ // Try to rename again, just for kicks. It should still fail.
+ ExpectPermissionError(base_file_->Rename(new_path));
+ }
+
+ // Now that TestDir is writeable again, we should be able to successfully
+ // rename the file.
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, base_file_->Rename(new_path));
+ ASSERT_EQ(new_path.value(), base_file_->full_path().value());
+ ASSERT_TRUE(AppendDataToFile(kTestData3));
+
+ base_file_->Finish();
+
+ // The contents of the file should be intact.
+ std::string file_contents;
+ std::string expected_contents(kTestData1);
+ expected_contents += kTestData2;
+ expected_contents += kTestData3;
+ ASSERT_TRUE(base::ReadFileToString(new_path, &file_contents));
+ EXPECT_EQ(expected_contents, file_contents);
+}
+
// Test that a failed write reports an error.
TEST_F(BaseFileTest, WriteWithError) {
base::FilePath path;
diff --git a/content/browser/download/base_file_win.cc b/content/browser/download/base_file_win.cc
index b2706e7..fec1454 100644
--- a/content/browser/download/base_file_win.cc
+++ b/content/browser/download/base_file_win.cc
@@ -9,6 +9,7 @@
#include <objbase.h>
#include <shellapi.h>
+#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/metrics/histogram.h"
@@ -25,6 +26,8 @@ namespace {
const int kAllSpecialShFileOperationCodes[] = {
// Should be kept in sync with the case statement below.
ERROR_ACCESS_DENIED,
+ ERROR_SHARING_VIOLATION,
+ ERROR_INVALID_PARAMETER,
0x71,
0x72,
0x73,
@@ -68,11 +71,28 @@ DownloadInterruptReason MapShFileOperationCodes(int code) {
// This switch statement should be kept in sync with the list of codes
// above.
switch (code) {
- // Not a pre-Win32 error code; here so that this particular
- // case shows up in our histograms. This is redundant with the
- // mapping function net::MapSystemError used later.
+ // Not a pre-Win32 error code; here so that this particular case shows up in
+ // our histograms. Unfortunately, it is used not just to signal actual
+ // ACCESS_DENIED errors, but many other errors as well. So we treat it as a
+ // transient error.
case ERROR_ACCESS_DENIED: // Access is denied.
- result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
+ result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
+ break;
+
+ // This isn't documented but returned from SHFileOperation. Sharing
+ // violations indicate that another process had the file open while we were
+ // trying to rename. Anti-virus is believed to be the cause of this error in
+ // the wild. Treated as a transient error on the assumption that the file
+ // will be made available for renaming at a later time.
+ case ERROR_SHARING_VIOLATION:
+ result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
+ break;
+
+ // This is also not a documented return value of SHFileOperation, but has
+ // been observed in the wild. We are treating it as a transient error based
+ // on the cases we have seen so far. See http://crbug.com/368455.
+ case ERROR_INVALID_PARAMETER:
+ result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
break;
// The source and destination files are the same file.
@@ -250,12 +270,20 @@ DownloadInterruptReason MapShFileOperationCodes(int code) {
arraysize(kAllSpecialShFileOperationCodes)));
}
+ if (result == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR) {
+ UMA_HISTOGRAM_CUSTOM_ENUMERATION(
+ "Download.MapWinShErrorTransientError", code,
+ base::CustomHistogram::ArrayToCustomRanges(
+ kAllSpecialShFileOperationCodes,
+ arraysize(kAllSpecialShFileOperationCodes)));
+ }
+
if (result != DOWNLOAD_INTERRUPT_REASON_NONE)
return result;
// If not one of the above codes, it should be a standard Windows error code.
- return ConvertNetErrorToInterruptReason(
- net::MapSystemError(code), DOWNLOAD_INTERRUPT_FROM_DISK);
+ return ConvertFileErrorToInterruptReason(
+ base::File::OSErrorToFileError(code));
}
// Maps a return code from ScanAndSaveDownloadedFile() to a
@@ -310,6 +338,7 @@ DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
move_info.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI |
FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;
+ base::TimeTicks now = base::TimeTicks::Now();
int result = SHFileOperation(&move_info);
DownloadInterruptReason interrupt_reason = DOWNLOAD_INTERRUPT_REASON_NONE;
diff --git a/content/browser/download/download_file_impl.cc b/content/browser/download/download_file_impl.cc
index e8cb761..91b25da 100644
--- a/content/browser/download/download_file_impl.cc
+++ b/content/browser/download/download_file_impl.cc
@@ -25,6 +25,12 @@ namespace content {
const int kUpdatePeriodMs = 500;
const int kMaxTimeBlockingFileThreadMs = 1000;
+// These constants control the default retry behavior for failing renames. Each
+// retry is performed after a delay that is twice the previous delay. The
+// initial delay is specified by kInitialRenameRetryDelayMs.
+const int kMaxRenameRetries = 3;
+const int kInitialRenameRetryDelayMs = 200;
+
int DownloadFile::number_active_objects_ = 0;
DownloadFileImpl::DownloadFileImpl(
@@ -36,20 +42,20 @@ DownloadFileImpl::DownloadFileImpl(
scoped_ptr<ByteStreamReader> stream,
const net::BoundNetLog& bound_net_log,
base::WeakPtr<DownloadDestinationObserver> observer)
- : file_(save_info->file_path,
- url,
- referrer_url,
- save_info->offset,
- calculate_hash,
- save_info->hash_state,
- save_info->file.Pass(),
- bound_net_log),
- default_download_directory_(default_download_directory),
- stream_reader_(stream.Pass()),
- bytes_seen_(0),
- bound_net_log_(bound_net_log),
- observer_(observer),
- weak_factory_(this) {
+ : file_(save_info->file_path,
+ url,
+ referrer_url,
+ save_info->offset,
+ calculate_hash,
+ save_info->hash_state,
+ save_info->file.Pass(),
+ bound_net_log),
+ default_download_directory_(default_download_directory),
+ stream_reader_(stream.Pass()),
+ bytes_seen_(0),
+ bound_net_log_(bound_net_log),
+ observer_(observer),
+ weak_factory_(this) {
}
DownloadFileImpl::~DownloadFileImpl() {
@@ -103,47 +109,84 @@ DownloadInterruptReason DownloadFileImpl::AppendDataToFile(
void DownloadFileImpl::RenameAndUniquify(
const base::FilePath& full_path,
const RenameCompletionCallback& callback) {
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
-
- base::FilePath new_path(full_path);
-
- int uniquifier = base::GetUniquePathNumber(
- new_path, base::FilePath::StringType());
- if (uniquifier > 0) {
- new_path = new_path.InsertBeforeExtensionASCII(
- base::StringPrintf(" (%d)", uniquifier));
- }
-
- DownloadInterruptReason reason = file_.Rename(new_path);
- if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) {
- // Make sure our information is updated, since we're about to
- // error out.
- SendUpdate();
+ RenameWithRetryInternal(
+ full_path, UNIQUIFY, kMaxRenameRetries, base::TimeTicks(), callback);
+}
- // Null out callback so that we don't do any more stream processing.
- stream_reader_->RegisterCallback(base::Closure());
+void DownloadFileImpl::RenameAndAnnotate(
+ const base::FilePath& full_path,
+ const RenameCompletionCallback& callback) {
+ RenameWithRetryInternal(full_path,
+ ANNOTATE_WITH_SOURCE_INFORMATION,
+ kMaxRenameRetries,
+ base::TimeTicks(),
+ callback);
+}
- new_path.clear();
- }
+base::TimeDelta DownloadFileImpl::GetRetryDelayForFailedRename(
+ int attempt_number) {
+ DCHECK_GE(attempt_number, 0);
+ // |delay| starts at kInitialRenameRetryDelayMs and increases by a factor of
+ // 2 at each subsequent retry. Assumes that |retries_left| starts at
+ // kMaxRenameRetries. Also assumes that kMaxRenameRetries is less than the
+ // number of bits in an int.
+ return base::TimeDelta::FromMilliseconds(kInitialRenameRetryDelayMs) *
+ (1 << attempt_number);
+}
- BrowserThread::PostTask(
- BrowserThread::UI, FROM_HERE,
- base::Bind(callback, reason, new_path));
+bool DownloadFileImpl::ShouldRetryFailedRename(DownloadInterruptReason reason) {
+ return reason == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
}
-void DownloadFileImpl::RenameAndAnnotate(
+void DownloadFileImpl::RenameWithRetryInternal(
const base::FilePath& full_path,
+ RenameOption option,
+ int retries_left,
+ base::TimeTicks time_of_first_failure,
const RenameCompletionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
base::FilePath new_path(full_path);
- DownloadInterruptReason reason = DOWNLOAD_INTERRUPT_REASON_NONE;
- // Short circuit null rename.
- if (full_path != file_.full_path())
- reason = file_.Rename(new_path);
+ if ((option & UNIQUIFY) && full_path != file_.full_path()) {
+ int uniquifier =
+ base::GetUniquePathNumber(new_path, base::FilePath::StringType());
+ if (uniquifier > 0)
+ new_path = new_path.InsertBeforeExtensionASCII(
+ base::StringPrintf(" (%d)", uniquifier));
+ }
+
+ DownloadInterruptReason reason = file_.Rename(new_path);
+
+ // Attempt to retry the rename if possible. If the rename failed and the
+ // subsequent open also failed, then in_progress() would be false. We don't
+ // try to retry renames if the in_progress() was false to begin with since we
+ // have less assurance that the file at file_.full_path() was the one we were
+ // working with.
+ if (ShouldRetryFailedRename(reason) && file_.in_progress() &&
+ retries_left > 0) {
+ int attempt_number = kMaxRenameRetries - retries_left;
+ BrowserThread::PostDelayedTask(
+ BrowserThread::FILE,
+ FROM_HERE,
+ base::Bind(&DownloadFileImpl::RenameWithRetryInternal,
+ weak_factory_.GetWeakPtr(),
+ full_path,
+ option,
+ --retries_left,
+ time_of_first_failure.is_null() ? base::TimeTicks::Now()
+ : time_of_first_failure,
+ callback),
+ GetRetryDelayForFailedRename(attempt_number));
+ return;
+ }
+
+ if (!time_of_first_failure.is_null())
+ RecordDownloadFileRenameResultAfterRetry(
+ base::TimeTicks::Now() - time_of_first_failure, reason);
- if (reason == DOWNLOAD_INTERRUPT_REASON_NONE) {
+ if (reason == DOWNLOAD_INTERRUPT_REASON_NONE &&
+ (option & ANNOTATE_WITH_SOURCE_INFORMATION)) {
// Doing the annotation after the rename rather than before leaves
// a very small window during which the file has the final name but
// hasn't been marked with the Mark Of The Web. However, it allows
diff --git a/content/browser/download/download_file_impl.h b/content/browser/download/download_file_impl.h
index 2ba3d8d..f7950ee 100644
--- a/content/browser/download/download_file_impl.h
+++ b/content/browser/download/download_file_impl.h
@@ -68,7 +68,36 @@ class CONTENT_EXPORT DownloadFileImpl : virtual public DownloadFile {
virtual DownloadInterruptReason AppendDataToFile(
const char* data, size_t data_len);
+ virtual base::TimeDelta GetRetryDelayForFailedRename(int attempt_number);
+
+ virtual bool ShouldRetryFailedRename(DownloadInterruptReason reason);
+
private:
+ friend class DownloadFileTest;
+
+ // Options for RenameWithRetryInternal.
+ enum RenameOption {
+ UNIQUIFY = 1 << 0, // If there's already a file on disk that conflicts with
+ // |new_path|, try to create a unique file by appending
+ // a uniquifier.
+ ANNOTATE_WITH_SOURCE_INFORMATION = 1 << 1
+ };
+
+ // Rename file_ to |new_path|.
+ // |option| specifies additional operations to be performed during the rename.
+ // See RenameOption above.
+ // |retries_left| indicates how many times to retry the operation if the
+ // rename fails with a transient error.
+ // |time_of_first_failure| Set to an empty base::TimeTicks during the first
+ // call. Once the first failure is seen, subsequent calls of
+ // RenameWithRetryInternal will have a non-empty value keeping track of
+ // the time of first observed failure. Used for UMA.
+ void RenameWithRetryInternal(const base::FilePath& new_path,
+ RenameOption option,
+ int retries_left,
+ base::TimeTicks time_of_first_failure,
+ const RenameCompletionCallback& callback);
+
// Send an update on our progress.
void SendUpdate();
diff --git a/content/browser/download/download_file_unittest.cc b/content/browser/download/download_file_unittest.cc
index 4246652..e2340fc 100644
--- a/content/browser/download/download_file_unittest.cc
+++ b/content/browser/download/download_file_unittest.cc
@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/message_loop/message_loop.h"
+#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/test_file_util.h"
#include "content/browser/browser_thread_impl.h"
@@ -59,6 +61,48 @@ class MockDownloadDestinationObserver : public DownloadDestinationObserver {
MATCHER(IsNullCallback, "") { return (arg.is_null()); }
+typedef void (DownloadFile::*DownloadFileRenameMethodType)(
+ const base::FilePath&,
+ const DownloadFile::RenameCompletionCallback&);
+
+// This is a test DownloadFileImpl that has no retry delay and, on Posix,
+// retries renames failed due to ACCESS_DENIED.
+class TestDownloadFileImpl : public DownloadFileImpl {
+ public:
+ TestDownloadFileImpl(scoped_ptr<DownloadSaveInfo> save_info,
+ const base::FilePath& default_downloads_directory,
+ const GURL& url,
+ const GURL& referrer_url,
+ bool calculate_hash,
+ scoped_ptr<ByteStreamReader> stream,
+ const net::BoundNetLog& bound_net_log,
+ base::WeakPtr<DownloadDestinationObserver> observer)
+ : DownloadFileImpl(save_info.Pass(),
+ default_downloads_directory,
+ url,
+ referrer_url,
+ calculate_hash,
+ stream.Pass(),
+ bound_net_log,
+ observer) {}
+
+ protected:
+ virtual base::TimeDelta GetRetryDelayForFailedRename(
+ int attempt_count) OVERRIDE {
+ return base::TimeDelta::FromMilliseconds(0);
+ }
+
+#if !defined(OS_WIN)
+ // On Posix, we don't encounter transient errors during renames, except
+ // possibly EAGAIN, which is difficult to replicate reliably. So we resort to
+ // simulating a transient error using ACCESS_DENIED instead.
+ virtual bool ShouldRetryFailedRename(
+ DownloadInterruptReason reason) OVERRIDE {
+ return reason == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
+ }
+#endif
+};
+
} // namespace
class DownloadFileTest : public testing::Test {
@@ -104,15 +148,15 @@ class DownloadFileTest : public testing::Test {
}
// Mock calls to this function are forwarded here.
- void RegisterCallback(base::Closure sink_callback) {
+ void RegisterCallback(const base::Closure& sink_callback) {
sink_callback_ = sink_callback;
}
- void SetInterruptReasonCallback(bool* was_called,
+ void SetInterruptReasonCallback(const base::Closure& closure,
DownloadInterruptReason* reason_p,
DownloadInterruptReason reason) {
- *was_called = true;
*reason_p = reason;
+ closure.Run();
}
bool CreateDownloadFile(int offset, bool calculate_hash) {
@@ -128,30 +172,29 @@ class DownloadFileTest : public testing::Test {
.RetiresOnSaturation();
scoped_ptr<DownloadSaveInfo> save_info(new DownloadSaveInfo());
- download_file_.reset(
- new DownloadFileImpl(save_info.Pass(),
- base::FilePath(),
- GURL(), // Source
- GURL(), // Referrer
- calculate_hash,
- scoped_ptr<ByteStreamReader>(input_stream_),
- net::BoundNetLog(),
- observer_factory_.GetWeakPtr()));
- download_file_->SetClientGuid(
- "12345678-ABCD-1234-DCBA-123456789ABC");
+ scoped_ptr<TestDownloadFileImpl> download_file_impl(
+ new TestDownloadFileImpl(save_info.Pass(),
+ base::FilePath(),
+ GURL(), // Source
+ GURL(), // Referrer
+ calculate_hash,
+ scoped_ptr<ByteStreamReader>(input_stream_),
+ net::BoundNetLog(),
+ observer_factory_.GetWeakPtr()));
+ download_file_impl->SetClientGuid("12345678-ABCD-1234-DCBA-123456789ABC");
+ download_file_ = download_file_impl.PassAs<DownloadFile>();
EXPECT_CALL(*input_stream_, Read(_, _))
.WillOnce(Return(ByteStreamReader::STREAM_EMPTY))
.RetiresOnSaturation();
base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
- bool called = false;
- DownloadInterruptReason result;
+ DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;
+ base::RunLoop loop_runner;
download_file_->Initialize(base::Bind(
&DownloadFileTest::SetInterruptReasonCallback,
- weak_ptr_factory.GetWeakPtr(), &called, &result));
- loop_.RunUntilIdle();
- EXPECT_TRUE(called);
+ weak_ptr_factory.GetWeakPtr(), loop_runner.QuitClosure(), &result));
+ loop_runner.Run();
::testing::Mock::VerifyAndClearExpectations(input_stream_);
return result == DOWNLOAD_INTERRUPT_REASON_NONE;
@@ -243,42 +286,42 @@ class DownloadFileTest : public testing::Test {
DownloadInterruptReason RenameAndUniquify(
const base::FilePath& full_path,
base::FilePath* result_path_p) {
- base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
- DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
- bool callback_was_called(false);
- base::FilePath result_path;
+ return InvokeRenameMethodAndWaitForCallback(
+ &DownloadFile::RenameAndUniquify, full_path, result_path_p);
+ }
- download_file_->RenameAndUniquify(
- full_path, base::Bind(&DownloadFileTest::SetRenameResult,
- weak_ptr_factory.GetWeakPtr(),
- &callback_was_called,
- &result_reason, result_path_p));
- loop_.RunUntilIdle();
+ DownloadInterruptReason RenameAndAnnotate(
+ const base::FilePath& full_path,
+ base::FilePath* result_path_p) {
+ return InvokeRenameMethodAndWaitForCallback(
+ &DownloadFile::RenameAndAnnotate, full_path, result_path_p);
+ }
- EXPECT_TRUE(callback_was_called);
- return result_reason;
+ void ExpectPermissionError(DownloadInterruptReason err) {
+ EXPECT_TRUE(err == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR ||
+ err == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED)
+ << "Interrupt reason = " << err;
}
- DownloadInterruptReason RenameAndAnnotate(
+ protected:
+ DownloadInterruptReason InvokeRenameMethodAndWaitForCallback(
+ DownloadFileRenameMethodType method,
const base::FilePath& full_path,
base::FilePath* result_path_p) {
- base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
- bool callback_was_called(false);
base::FilePath result_path;
- download_file_->RenameAndAnnotate(
- full_path, base::Bind(&DownloadFileTest::SetRenameResult,
- weak_ptr_factory.GetWeakPtr(),
- &callback_was_called,
- &result_reason, result_path_p));
- loop_.RunUntilIdle();
-
- EXPECT_TRUE(callback_was_called);
+ base::RunLoop loop_runner;
+ ((*download_file_).*method)(full_path,
+ base::Bind(&DownloadFileTest::SetRenameResult,
+ base::Unretained(this),
+ loop_runner.QuitClosure(),
+ &result_reason,
+ result_path_p));
+ loop_runner.Run();
return result_reason;
}
- protected:
scoped_ptr<StrictMock<MockDownloadDestinationObserver> > observer_;
base::WeakPtrFactory<DownloadDestinationObserver> observer_factory_;
@@ -300,17 +343,16 @@ class DownloadFileTest : public testing::Test {
base::MessageLoop loop_;
private:
- void SetRenameResult(bool* called_p,
+ void SetRenameResult(const base::Closure& closure,
DownloadInterruptReason* reason_p,
base::FilePath* result_path_p,
DownloadInterruptReason reason,
const base::FilePath& result_path) {
- if (called_p)
- *called_p = true;
if (reason_p)
*reason_p = reason;
if (result_path_p)
*result_path_p = result_path;
+ closure.Run();
}
// UI thread.
@@ -322,6 +364,33 @@ class DownloadFileTest : public testing::Test {
std::string expected_data_;
};
+// DownloadFile::RenameAndAnnotate and DownloadFile::RenameAndUniquify have a
+// considerable amount of functional overlap. In order to re-use test logic, we
+// are going to introduce this value parameterized test fixture. It will take a
+// DownloadFileRenameMethodType value which can be either of the two rename
+// methods.
+class DownloadFileTestWithRename
+ : public DownloadFileTest,
+ public ::testing::WithParamInterface<DownloadFileRenameMethodType> {
+ protected:
+ DownloadInterruptReason InvokeSelectedRenameMethod(
+ const base::FilePath& full_path,
+ base::FilePath* result_path_p) {
+ return InvokeRenameMethodAndWaitForCallback(
+ GetParam(), full_path, result_path_p);
+ }
+};
+
+// And now instantiate all DownloadFileTestWithRename tests using both
+// DownloadFile rename methods. Each test of the form
+// DownloadFileTestWithRename.<FooTest> will be instantiated once with
+// RenameAndAnnotate as the value parameter and once with RenameAndUniquify as
+// the value parameter.
+INSTANTIATE_TEST_CASE_P(DownloadFile,
+ DownloadFileTestWithRename,
+ ::testing::Values(&DownloadFile::RenameAndAnnotate,
+ &DownloadFile::RenameAndUniquify));
+
const char* DownloadFileTest::kTestData1 =
"Let's write some data to the file!\n";
const char* DownloadFileTest::kTestData2 = "Writing more data.\n";
@@ -335,7 +404,7 @@ const int DownloadFileTest::kDummyRequestId = 67;
// Rename the file before any data is downloaded, after some has, after it all
// has, and after it's closed.
-TEST_F(DownloadFileTest, RenameFileFinal) {
+TEST_P(DownloadFileTestWithRename, RenameFileFinal) {
ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path));
@@ -343,12 +412,11 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
base::FilePath path_2(initial_path.InsertBeforeExtensionASCII("_2"));
base::FilePath path_3(initial_path.InsertBeforeExtensionASCII("_3"));
base::FilePath path_4(initial_path.InsertBeforeExtensionASCII("_4"));
- base::FilePath path_5(initial_path.InsertBeforeExtensionASCII("_5"));
base::FilePath output_path;
// Rename the file before downloading any data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
- RenameAndUniquify(path_1, &output_path));
+ InvokeSelectedRenameMethod(path_1, &output_path));
base::FilePath renamed_path = download_file_->FullPath();
EXPECT_EQ(path_1, renamed_path);
EXPECT_EQ(path_1, output_path);
@@ -363,7 +431,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading some data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
- RenameAndUniquify(path_2, &output_path));
+ InvokeSelectedRenameMethod(path_2, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_2, renamed_path);
EXPECT_EQ(path_2, output_path);
@@ -377,7 +445,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading all the data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
- RenameAndUniquify(path_3, &output_path));
+ InvokeSelectedRenameMethod(path_3, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_3, renamed_path);
EXPECT_EQ(path_3, output_path);
@@ -394,7 +462,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading all the data and closing the file.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
- RenameAndUniquify(path_4, &output_path));
+ InvokeSelectedRenameMethod(path_4, &output_path));
renamed_path = download_file_->FullPath();
EXPECT_EQ(path_4, renamed_path);
EXPECT_EQ(path_4, output_path);
@@ -407,29 +475,42 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
EXPECT_TRUE(download_file_->GetHash(&hash));
EXPECT_EQ(kDataHash, base::HexEncode(hash.data(), hash.size()));
- // Check that a rename with overwrite to an existing file succeeds.
- std::string file_contents;
- ASSERT_FALSE(base::PathExists(path_5));
+ DestroyDownloadFile(0);
+}
+
+// Test to make sure the rename overwrites when requested. This is separate from
+// the above test because it only applies to RenameAndAnnotate().
+// RenameAndUniquify() doesn't overwrite by design.
+TEST_F(DownloadFileTest, RenameOverwrites) {
+ ASSERT_TRUE(CreateDownloadFile(0, true));
+ base::FilePath initial_path(download_file_->FullPath());
+ EXPECT_TRUE(base::PathExists(initial_path));
+ base::FilePath path_1(initial_path.InsertBeforeExtensionASCII("_1"));
+
+ ASSERT_FALSE(base::PathExists(path_1));
static const char file_data[] = "xyzzy";
- ASSERT_EQ(static_cast<int>(sizeof(file_data) - 1),
- base::WriteFile(path_5, file_data, sizeof(file_data) - 1));
- ASSERT_TRUE(base::PathExists(path_5));
- EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents));
- EXPECT_EQ(std::string(file_data), file_contents);
+ ASSERT_EQ(static_cast<int>(sizeof(file_data)),
+ base::WriteFile(path_1, file_data, sizeof(file_data)));
+ ASSERT_TRUE(base::PathExists(path_1));
+ base::FilePath new_path;
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
- RenameAndAnnotate(path_5, &output_path));
- EXPECT_EQ(path_5, output_path);
+ RenameAndAnnotate(path_1, &new_path));
+ EXPECT_EQ(path_1.value(), new_path.value());
- file_contents = "";
- EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents));
+ std::string file_contents;
+ ASSERT_TRUE(base::ReadFileToString(new_path, &file_contents));
EXPECT_NE(std::string(file_data), file_contents);
+ FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
+ loop_.RunUntilIdle();
DestroyDownloadFile(0);
}
// Test to make sure the rename uniquifies if we aren't overwriting
-// and there's a file where we're aiming.
+// and there's a file where we're aiming. As above, not a
+// DownloadFileTestWithRename test because this only applies to
+// RenameAndUniquify().
TEST_F(DownloadFileTest, RenameUniquifies) {
ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath());
@@ -451,16 +532,35 @@ TEST_F(DownloadFileTest, RenameUniquifies) {
DestroyDownloadFile(0);
}
+// Test that RenameAndUniquify doesn't try to uniquify in the case where the
+// target filename is the same as the current filename.
+TEST_F(DownloadFileTest, RenameRecognizesSelfConflict) {
+ ASSERT_TRUE(CreateDownloadFile(0, true));
+ base::FilePath initial_path(download_file_->FullPath());
+ EXPECT_TRUE(base::PathExists(initial_path));
+
+ base::FilePath new_path;
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
+ RenameAndUniquify(initial_path, &new_path));
+ EXPECT_TRUE(base::PathExists(initial_path));
+
+ FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
+ loop_.RunUntilIdle();
+ DestroyDownloadFile(0);
+ EXPECT_EQ(initial_path.value(), new_path.value());
+}
+
// Test to make sure we get the proper error on failure.
-TEST_F(DownloadFileTest, RenameError) {
+TEST_P(DownloadFileTestWithRename, RenameError) {
ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath());
// Create a subdirectory.
- base::FilePath tempdir(
- initial_path.DirName().Append(FILE_PATH_LITERAL("tempdir")));
- ASSERT_TRUE(base::CreateDirectory(tempdir));
- base::FilePath target_path(tempdir.Append(initial_path.BaseName()));
+ base::FilePath target_dir(
+ initial_path.DirName().Append(FILE_PATH_LITERAL("TargetDir")));
+ ASSERT_FALSE(base::DirectoryExists(target_dir));
+ ASSERT_TRUE(base::CreateDirectory(target_dir));
+ base::FilePath target_path(target_dir.Append(initial_path.BaseName()));
// Targets
base::FilePath target_path_suffixed(
@@ -470,13 +570,12 @@ TEST_F(DownloadFileTest, RenameError) {
// Make the directory unwritable and try to rename within it.
{
- base::FilePermissionRestorer restorer(tempdir);
- ASSERT_TRUE(base::MakeFileUnwritable(tempdir));
+ base::FilePermissionRestorer restorer(target_dir);
+ ASSERT_TRUE(base::MakeFileUnwritable(target_dir));
// Expect nulling out of further processing.
EXPECT_CALL(*input_stream_, RegisterCallback(IsNullCallback()));
- EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
- RenameAndAnnotate(target_path, NULL));
+ ExpectPermissionError(InvokeSelectedRenameMethod(target_path, NULL));
EXPECT_FALSE(base::PathExists(target_path_suffixed));
}
@@ -485,6 +584,98 @@ TEST_F(DownloadFileTest, RenameError) {
DestroyDownloadFile(0);
}
+namespace {
+
+void TestRenameCompletionCallback(const base::Closure& closure,
+ bool* did_run_callback,
+ DownloadInterruptReason interrupt_reason,
+ const base::FilePath& new_path) {
+ EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason);
+ *did_run_callback = true;
+ closure.Run();
+}
+
+} // namespace
+
+// Test that the retry logic works. This test assumes that DownloadFileImpl will
+// post tasks to the current message loop (acting as the FILE thread)
+// asynchronously to retry the renames. We will stuff RunLoop::QuitClosures()
+// in between the retry tasks to stagger them and then allow the rename to
+// succeed.
+//
+// Note that there is only one queue of tasks to run, and that is in the tests'
+// base::MessageLoop::current(). Each RunLoop processes that queue until it sees
+// a QuitClosure() targeted at itself, at which point it stops processing.
+TEST_P(DownloadFileTestWithRename, RenameWithErrorRetry) {
+ ASSERT_TRUE(CreateDownloadFile(0, true));
+ base::FilePath initial_path(download_file_->FullPath());
+
+ // Create a subdirectory.
+ base::FilePath target_dir(
+ initial_path.DirName().Append(FILE_PATH_LITERAL("TargetDir")));
+ ASSERT_FALSE(base::DirectoryExists(target_dir));
+ ASSERT_TRUE(base::CreateDirectory(target_dir));
+ base::FilePath target_path(target_dir.Append(initial_path.BaseName()));
+
+ bool did_run_callback = false;
+
+ // Each RunLoop can be used the run the MessageLoop until the corresponding
+ // QuitClosure() is run. This one is used to produce the QuitClosure() that
+ // will be run when the entire rename operation is complete.
+ base::RunLoop succeeding_run;
+ {
+ // (Scope for the base::File or base::FilePermissionRestorer below.)
+#if defined(OS_WIN)
+ // On Windows we test with an actual transient error, a sharing violation.
+ // The rename will fail because we are holding the file open for READ. On
+ // Posix this doesn't cause a failure.
+ base::File locked_file(initial_path,
+ base::File::FLAG_OPEN | base::File::FLAG_READ);
+ ASSERT_TRUE(locked_file.IsValid());
+#else
+ // Simulate a transient failure by revoking write permission for target_dir.
+ // The TestDownloadFileImpl class treats this error as transient even though
+ // DownloadFileImpl itself doesn't.
+ base::FilePermissionRestorer restore_permissions_for(target_dir);
+ ASSERT_TRUE(base::MakeFileUnwritable(target_dir));
+#endif
+
+ // The Rename() should fail here and enqueue a retry task without invoking
+ // the completion callback.
+ ((*download_file_).*GetParam())(target_path,
+ base::Bind(&TestRenameCompletionCallback,
+ succeeding_run.QuitClosure(),
+ &did_run_callback));
+ EXPECT_FALSE(did_run_callback);
+
+ base::RunLoop first_failing_run;
+ // Queue the QuitClosure() on the MessageLoop now. Any tasks queued by the
+ // Rename() will be in front of the QuitClosure(). Running the message loop
+ // now causes the just the first retry task to be run. The rename still
+ // fails, so another retry task would get queued behind the QuitClosure().
+ base::MessageLoop::current()->PostTask(FROM_HERE,
+ first_failing_run.QuitClosure());
+ first_failing_run.Run();
+ EXPECT_FALSE(did_run_callback);
+
+ // Running another loop should have the same effect as the above as long as
+ // kMaxRenameRetries is greater than 2.
+ base::RunLoop second_failing_run;
+ base::MessageLoop::current()->PostTask(FROM_HERE,
+ second_failing_run.QuitClosure());
+ second_failing_run.Run();
+ EXPECT_FALSE(did_run_callback);
+ }
+
+ // This time the QuitClosure from succeeding_run should get executed.
+ succeeding_run.Run();
+ EXPECT_TRUE(did_run_callback);
+
+ FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
+ loop_.RunUntilIdle();
+ DestroyDownloadFile(0);
+}
+
// Various tests of the StreamActive method.
TEST_F(DownloadFileTest, StreamEmptySuccess) {
ASSERT_TRUE(CreateDownloadFile(0, true));
diff --git a/content/browser/download/download_interrupt_reasons_impl.cc b/content/browser/download/download_interrupt_reasons_impl.cc
index 9a01f48..2ed8f14 100644
--- a/content/browser/download/download_interrupt_reasons_impl.cc
+++ b/content/browser/download/download_interrupt_reasons_impl.cc
@@ -8,6 +8,35 @@
namespace content {
+DownloadInterruptReason ConvertFileErrorToInterruptReason(
+ base::File::Error file_error) {
+ switch (file_error) {
+ case base::File::FILE_OK:
+ return DOWNLOAD_INTERRUPT_REASON_NONE;
+
+ case base::File::FILE_ERROR_IN_USE:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
+
+ case base::File::FILE_ERROR_ACCESS_DENIED:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
+
+ case base::File::FILE_ERROR_TOO_MANY_OPENED:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
+
+ case base::File::FILE_ERROR_NO_MEMORY:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
+
+ case base::File::FILE_ERROR_NO_SPACE:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE;
+
+ case base::File::FILE_ERROR_SECURITY:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
+
+ default:
+ return DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
+ }
+}
+
DownloadInterruptReason ConvertNetErrorToInterruptReason(
net::Error net_error, DownloadInterruptSource source) {
switch (net_error) {
diff --git a/content/browser/download/download_interrupt_reasons_impl.h b/content/browser/download/download_interrupt_reasons_impl.h
index 137dcb2..61ccae1 100644
--- a/content/browser/download/download_interrupt_reasons_impl.h
+++ b/content/browser/download/download_interrupt_reasons_impl.h
@@ -5,6 +5,7 @@
#ifndef CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_
#define CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_
+#include "base/files/file.h"
#include "content/public/browser/download_interrupt_reasons.h"
#include "net/base/net_errors.h"
@@ -22,6 +23,10 @@ enum DownloadInterruptSource {
DownloadInterruptReason CONTENT_EXPORT ConvertNetErrorToInterruptReason(
net::Error file_error, DownloadInterruptSource source);
+// Safe to call from any thread.
+DownloadInterruptReason CONTENT_EXPORT ConvertFileErrorToInterruptReason(
+ base::File::Error file_error);
+
} // namespace content
#endif // CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_
diff --git a/content/browser/download/download_stats.cc b/content/browser/download/download_stats.cc
index 66716ca..d88eef2 100644
--- a/content/browser/download/download_stats.cc
+++ b/content/browser/download/download_stats.cc
@@ -611,6 +611,18 @@ void RecordFileBandwidth(size_t length,
disk_write_time_ms * 100 / elapsed_time_ms);
}
+void RecordDownloadFileRenameResultAfterRetry(
+ base::TimeDelta time_since_first_failure,
+ DownloadInterruptReason interrupt_reason) {
+ if (interrupt_reason == DOWNLOAD_INTERRUPT_REASON_NONE) {
+ UMA_HISTOGRAM_TIMES("Download.TimeToRenameSuccessAfterInitialFailure",
+ time_since_first_failure);
+ } else {
+ UMA_HISTOGRAM_TIMES("Download.TimeToRenameFailureAfterInitialFailure",
+ time_since_first_failure);
+ }
+}
+
void RecordSavePackageEvent(SavePackageEvent event) {
UMA_HISTOGRAM_ENUMERATION("Download.SavePackage",
event,
diff --git a/content/browser/download/download_stats.h b/content/browser/download/download_stats.h
index 65fad8d..3ae2612 100644
--- a/content/browser/download/download_stats.h
+++ b/content/browser/download/download_stats.h
@@ -200,6 +200,11 @@ void RecordFileBandwidth(size_t length,
base::TimeDelta disk_write_time,
base::TimeDelta elapsed_time);
+// Record the result of a download file rename.
+void RecordDownloadFileRenameResultAfterRetry(
+ base::TimeDelta time_since_first_failure,
+ DownloadInterruptReason interrupt_reason);
+
enum SavePackageEvent {
// The user has started to save a page as a package.
SAVE_PACKAGE_STARTED,
diff --git a/content/public/browser/download_interrupt_reason_values.h b/content/public/browser/download_interrupt_reason_values.h
index 7fdedd3..bcb21b3 100644
--- a/content/public/browser/download_interrupt_reason_values.h
+++ b/content/public/browser/download_interrupt_reason_values.h
@@ -14,7 +14,6 @@
INTERRUPT_REASON(FILE_FAILED, 1)
// The file cannot be accessed due to security restrictions.
-// The file cannot be accessed.
// "Access Denied".
INTERRUPT_REASON(FILE_ACCESS_DENIED, 2)
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index e42153b..c82cd19 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -5567,6 +5567,15 @@ Therefore, the affected-histogram name has to have at least one dot in it.
</summary>
</histogram>
+<histogram name="Download.MapWinShErrorTransientError"
+ enum="SpecialShFileOperationCodes">
+ <owner>asanka@chromium.org</owner>
+ <summary>
+ Windows error that produced a DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR
+ result in MapShFileOperationCodes().
+ </summary>
+</histogram>
+
<histogram name="Download.OnChanged">
<owner>asanka@chromium.org</owner>
<summary>
@@ -5700,6 +5709,24 @@ Therefore, the affected-histogram name has to have at least one dot in it.
<summary>Time between the start of a download and its completion.</summary>
</histogram>
+<histogram name="Download.TimeToRenameFailureAfterInitialFailure"
+ units="milliseconds">
+ <owner>asanka@chromium.org</owner>
+ <summary>
+ Time elapsed until a retried download file rename operation failed for the
+ last time after the initial rename failed.
+ </summary>
+</histogram>
+
+<histogram name="Download.TimeToRenameSuccessAfterInitialFailure"
+ units="milliseconds">
+ <owner>asanka@chromium.org</owner>
+ <summary>
+ Time elapsed until a retried download file rename operation succeeded after
+ the initial rename failed.
+ </summary>
+</histogram>
+
<histogram name="Download.UserDiscard" enum="DownloadItem.DangerType">
<owner>asanka@chromium.org</owner>
<owner>felt@chromium.org</owner>
@@ -50900,7 +50927,9 @@ To add a new entry, add it with any value and run test to compute valid value.
<enum name="SpecialShFileOperationCodes" type="int">
<summary>Legacy error codes still returned by |ShFileOperation()|</summary>
- <int value="5" label="Access denied"/>
+ <int value="5" label="Access denied (Win32)"/>
+ <int value="32" label="Sharing violation (Win32)"/>
+ <int value="87" label="Invalid parameter (Win32)"/>
<int value="113" label="Source and Destination are same file"/>
<int value="114" label="Multiple source mapped to single destination"/>
<int value="115" label="Rename to different directory"/>