Commit 5700ef4d authored by asanka@chromium.org's avatar asanka@chromium.org

[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

Review URL: https://codereview.chromium.org/319603003

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@278483 0039d316-1c4b-4281-b951-d872f2087c98
parent cad4d0d4
...@@ -159,15 +159,18 @@ DownloadInterruptReason BaseFile::Rename(const base::FilePath& new_path) { ...@@ -159,15 +159,18 @@ DownloadInterruptReason BaseFile::Rename(const base::FilePath& new_path) {
// permissions / security descriptors that makes sense in the new directory. // permissions / security descriptors that makes sense in the new directory.
rename_result = MoveFileAndAdjustPermissions(new_path); rename_result = MoveFileAndAdjustPermissions(new_path);
if (rename_result == DOWNLOAD_INTERRUPT_REASON_NONE) { if (rename_result == DOWNLOAD_INTERRUPT_REASON_NONE)
full_path_ = new_path; full_path_ = new_path;
// Re-open the file if we were still using it.
// 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) if (was_in_progress)
rename_result = Open(); open_result = Open();
}
bound_net_log_.EndEvent(net::NetLog::TYPE_DOWNLOAD_FILE_RENAMED); 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() { void BaseFile::Detach() {
...@@ -326,11 +329,10 @@ DownloadInterruptReason BaseFile::LogSystemError( ...@@ -326,11 +329,10 @@ DownloadInterruptReason BaseFile::LogSystemError(
const char* operation, const char* operation,
logging::SystemErrorCode os_error) { logging::SystemErrorCode os_error) {
// There's no direct conversion from a system error to an interrupt reason. // 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( return LogInterruptReason(
operation, os_error, operation, os_error,
ConvertNetErrorToInterruptReason( ConvertFileErrorToInterruptReason(file_error));
net_error, DOWNLOAD_INTERRUPT_FROM_DISK));
} }
DownloadInterruptReason BaseFile::LogInterruptReason( DownloadInterruptReason BaseFile::LogInterruptReason(
......
...@@ -56,7 +56,10 @@ class CONTENT_EXPORT BaseFile { ...@@ -56,7 +56,10 @@ class CONTENT_EXPORT BaseFile {
DownloadInterruptReason AppendDataToFile(const char* data, size_t data_len); DownloadInterruptReason AppendDataToFile(const char* data, size_t data_len);
// Rename the download file. Returns a DownloadInterruptReason indicating the // 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); virtual DownloadInterruptReason Rename(const base::FilePath& full_path);
// Detach the file so it is not deleted on destruction. // Detach the file so it is not deleted on destruction.
...@@ -79,8 +82,15 @@ class CONTENT_EXPORT BaseFile { ...@@ -79,8 +82,15 @@ class CONTENT_EXPORT BaseFile {
// Windows to ensure the correct app client ID is available. // Windows to ensure the correct app client ID is available.
DownloadInterruptReason AnnotateWithSourceInformation(); 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(); } 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_; } int64 bytes_so_far() const { return bytes_so_far_; }
// Fills |hash| with the hash digest for the file. // Fills |hash| with the hash digest for the file.
......
...@@ -27,8 +27,6 @@ DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions( ...@@ -27,8 +27,6 @@ DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
if (!stat_succeeded) if (!stat_succeeded)
LogSystemError("stat", errno); 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)) if (!base::Move(full_path_, new_path))
return LogSystemError("Move", errno); return LogSystemError("Move", errno);
......
...@@ -476,6 +476,58 @@ TEST_F(BaseFileTest, RenameWithError) { ...@@ -476,6 +476,58 @@ TEST_F(BaseFileTest, RenameWithError) {
base_file_->Finish(); 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();
{
file_util::PermissionRestorer restore_permissions_for(test_dir);
ASSERT_TRUE(file_util::MakeFileUnwritable(test_dir));
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
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 with
// ACCESS_DENIED.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
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 that a failed write reports an error.
TEST_F(BaseFileTest, WriteWithError) { TEST_F(BaseFileTest, WriteWithError) {
base::FilePath path; base::FilePath path;
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#include <shellapi.h> #include <shellapi.h>
#include "base/file_util.h" #include "base/file_util.h"
#include "base/files/file.h"
#include "base/guid.h" #include "base/guid.h"
#include "base/metrics/histogram.h" #include "base/metrics/histogram.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
...@@ -25,6 +26,8 @@ namespace { ...@@ -25,6 +26,8 @@ namespace {
const int kAllSpecialShFileOperationCodes[] = { const int kAllSpecialShFileOperationCodes[] = {
// Should be kept in sync with the case statement below. // Should be kept in sync with the case statement below.
ERROR_ACCESS_DENIED, ERROR_ACCESS_DENIED,
ERROR_SHARING_VIOLATION,
ERROR_INVALID_PARAMETER,
0x71, 0x71,
0x72, 0x72,
0x73, 0x73,
...@@ -69,10 +72,25 @@ DownloadInterruptReason MapShFileOperationCodes(int code) { ...@@ -69,10 +72,25 @@ DownloadInterruptReason MapShFileOperationCodes(int code) {
// above. // above.
switch (code) { switch (code) {
// Not a pre-Win32 error code; here so that this particular // Not a pre-Win32 error code; here so that this particular
// case shows up in our histograms. This is redundant with the // case shows up in our histograms.
// mapping function net::MapSystemError used later.
case ERROR_ACCESS_DENIED: // Access is denied. 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; break;
// The source and destination files are the same file. // The source and destination files are the same file.
...@@ -250,12 +268,20 @@ DownloadInterruptReason MapShFileOperationCodes(int code) { ...@@ -250,12 +268,20 @@ DownloadInterruptReason MapShFileOperationCodes(int code) {
arraysize(kAllSpecialShFileOperationCodes))); 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) if (result != DOWNLOAD_INTERRUPT_REASON_NONE)
return result; return result;
// If not one of the above codes, it should be a standard Windows error code. // If not one of the above codes, it should be a standard Windows error code.
return ConvertNetErrorToInterruptReason( return ConvertFileErrorToInterruptReason(
net::MapSystemError(code), DOWNLOAD_INTERRUPT_FROM_DISK); base::File::OSErrorToFileError(code));
} }
// Maps a return code from ScanAndSaveDownloadedFile() to a // Maps a return code from ScanAndSaveDownloadedFile() to a
......
...@@ -25,6 +25,12 @@ namespace content { ...@@ -25,6 +25,12 @@ namespace content {
const int kUpdatePeriodMs = 500; const int kUpdatePeriodMs = 500;
const int kMaxTimeBlockingFileThreadMs = 1000; 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; int DownloadFile::number_active_objects_ = 0;
DownloadFileImpl::DownloadFileImpl( DownloadFileImpl::DownloadFileImpl(
...@@ -103,47 +109,84 @@ DownloadInterruptReason DownloadFileImpl::AppendDataToFile( ...@@ -103,47 +109,84 @@ DownloadInterruptReason DownloadFileImpl::AppendDataToFile(
void DownloadFileImpl::RenameAndUniquify( void DownloadFileImpl::RenameAndUniquify(
const base::FilePath& full_path, const base::FilePath& full_path,
const RenameCompletionCallback& callback) { const RenameCompletionCallback& callback) {
RenameWithRetryInternal(
full_path, UNIQUIFY, kMaxRenameRetries, base::TimeTicks(), callback);
}
void DownloadFileImpl::RenameAndAnnotate(
const base::FilePath& full_path,
const RenameCompletionCallback& callback) {
RenameWithRetryInternal(full_path,
ANNOTATE_WITH_SOURCE_INFORMATION,
kMaxRenameRetries,
base::TimeTicks(),
callback);
}
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);
}
bool DownloadFileImpl::ShouldRetryFailedRename(DownloadInterruptReason reason) {
return reason == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
}
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)); DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
base::FilePath new_path(full_path); base::FilePath new_path(full_path);
int uniquifier = base::GetUniquePathNumber( if ((option & UNIQUIFY) && full_path != file_.full_path()) {
new_path, base::FilePath::StringType()); int uniquifier =
if (uniquifier > 0) { base::GetUniquePathNumber(new_path, base::FilePath::StringType());
if (uniquifier > 0)
new_path = new_path.InsertBeforeExtensionASCII( new_path = new_path.InsertBeforeExtensionASCII(
base::StringPrintf(" (%d)", uniquifier)); base::StringPrintf(" (%d)", uniquifier));
} }
DownloadInterruptReason reason = file_.Rename(new_path); 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();
// Null out callback so that we don't do any more stream processing. // Attempt to retry the rename if possible. If the rename failed and the
stream_reader_->RegisterCallback(base::Closure()); // 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
new_path.clear(); // 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;
} }
BrowserThread::PostTask( if (!time_of_first_failure.is_null())
BrowserThread::UI, FROM_HERE, RecordDownloadFileRenameResultAfterRetry(
base::Bind(callback, reason, new_path)); base::TimeTicks::Now() - time_of_first_failure, reason);
}
void DownloadFileImpl::RenameAndAnnotate(
const base::FilePath& full_path,
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 (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 // Doing the annotation after the rename rather than before leaves
// a very small window during which the file has the final name but // 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 // hasn't been marked with the Mark Of The Web. However, it allows
......
...@@ -68,7 +68,36 @@ class CONTENT_EXPORT DownloadFileImpl : virtual public DownloadFile { ...@@ -68,7 +68,36 @@ class CONTENT_EXPORT DownloadFileImpl : virtual public DownloadFile {
virtual DownloadInterruptReason AppendDataToFile( virtual DownloadInterruptReason AppendDataToFile(
const char* data, size_t data_len); const char* data, size_t data_len);
virtual base::TimeDelta GetRetryDelayForFailedRename(int attempt_number);
virtual bool ShouldRetryFailedRename(DownloadInterruptReason reason);
private: 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. // Send an update on our progress.
void SendUpdate(); void SendUpdate();
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "base/file_util.h" #include "base/file_util.h"
#include "base/files/file.h"
#include "base/message_loop/message_loop.h" #include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/test/test_file_util.h" #include "base/test/test_file_util.h"
#include "content/browser/browser_thread_impl.h" #include "content/browser/browser_thread_impl.h"
...@@ -59,6 +61,48 @@ class MockDownloadDestinationObserver : public DownloadDestinationObserver { ...@@ -59,6 +61,48 @@ class MockDownloadDestinationObserver : public DownloadDestinationObserver {
MATCHER(IsNullCallback, "") { return (arg.is_null()); } 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 } // namespace
class DownloadFileTest : public testing::Test { class DownloadFileTest : public testing::Test {
...@@ -104,15 +148,15 @@ class DownloadFileTest : public testing::Test { ...@@ -104,15 +148,15 @@ class DownloadFileTest : public testing::Test {
} }
// Mock calls to this function are forwarded here. // 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; sink_callback_ = sink_callback;
} }
void SetInterruptReasonCallback(bool* was_called, void SetInterruptReasonCallback(const base::Closure& closure,
DownloadInterruptReason* reason_p, DownloadInterruptReason* reason_p,
DownloadInterruptReason reason) { DownloadInterruptReason reason) {
*was_called = true;
*reason_p = reason; *reason_p = reason;
closure.Run();
} }
bool CreateDownloadFile(int offset, bool calculate_hash) { bool CreateDownloadFile(int offset, bool calculate_hash) {
...@@ -128,8 +172,8 @@ class DownloadFileTest : public testing::Test { ...@@ -128,8 +172,8 @@ class DownloadFileTest : public testing::Test {
.RetiresOnSaturation(); .RetiresOnSaturation();
scoped_ptr<DownloadSaveInfo> save_info(new DownloadSaveInfo()); scoped_ptr<DownloadSaveInfo> save_info(new DownloadSaveInfo());
download_file_.reset( scoped_ptr<TestDownloadFileImpl> download_file_impl(
new DownloadFileImpl(save_info.Pass(), new TestDownloadFileImpl(save_info.Pass(),
base::FilePath(), base::FilePath(),
GURL(), // Source GURL(), // Source
GURL(), // Referrer GURL(), // Referrer
...@@ -137,21 +181,20 @@ class DownloadFileTest : public testing::Test { ...@@ -137,21 +181,20 @@ class DownloadFileTest : public testing::Test {
scoped_ptr<ByteStreamReader>(input_stream_), scoped_ptr<ByteStreamReader>(input_stream_),
net::BoundNetLog(), net::BoundNetLog(),
observer_factory_.GetWeakPtr())); observer_factory_.GetWeakPtr()));
download_file_->SetClientGuid( download_file_impl->SetClientGuid("12345678-ABCD-1234-DCBA-123456789ABC");
"12345678-ABCD-1234-DCBA-123456789ABC"); download_file_ = download_file_impl.PassAs<DownloadFile>();
EXPECT_CALL(*input_stream_, Read(_, _)) EXPECT_CALL(*input_stream_, Read(_, _))
.WillOnce(Return(ByteStreamReader::STREAM_EMPTY)) .WillOnce(Return(ByteStreamReader::STREAM_EMPTY))
.RetiresOnSaturation(); .RetiresOnSaturation();
base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this); base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this);
bool called = false; DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;
DownloadInterruptReason result; base::RunLoop loop_runner;
download_file_->Initialize(base::Bind( download_file_->Initialize(base::Bind(
&DownloadFileTest::SetInterruptReasonCallback, &DownloadFileTest::SetInterruptReasonCallback,
weak_ptr_factory.GetWeakPtr(), &called, &result)); weak_ptr_factory.GetWeakPtr(), loop_runner.QuitClosure(), &result));
loop_.RunUntilIdle(); loop_runner.Run();
EXPECT_TRUE(called);
::testing::Mock::VerifyAndClearExpectations(input_stream_); ::testing::Mock::VerifyAndClearExpectations(input_stream_);
return result == DOWNLOAD_INTERRUPT_REASON_NONE; return result == DOWNLOAD_INTERRUPT_REASON_NONE;
...@@ -243,42 +286,36 @@ class DownloadFileTest : public testing::Test { ...@@ -243,42 +286,36 @@ class DownloadFileTest : public testing::Test {
DownloadInterruptReason RenameAndUniquify( DownloadInterruptReason RenameAndUniquify(
const base::FilePath& full_path, const base::FilePath& full_path,
base::FilePath* result_path_p) { base::FilePath* result_path_p) {
base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this); return InvokeRenameMethodAndWaitForCallback(
DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE); &DownloadFile::RenameAndUniquify, full_path, result_path_p);
bool callback_was_called(false);
base::FilePath result_path;
download_file_->RenameAndUniquify(
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);
return result_reason;
} }
DownloadInterruptReason RenameAndAnnotate( DownloadInterruptReason RenameAndAnnotate(
const base::FilePath& full_path, const base::FilePath& full_path,
base::FilePath* result_path_p) { base::FilePath* result_path_p) {
base::WeakPtrFactory<DownloadFileTest> weak_ptr_factory(this); return InvokeRenameMethodAndWaitForCallback(
&DownloadFile::RenameAndAnnotate, full_path, result_path_p);
}
protected:
DownloadInterruptReason InvokeRenameMethodAndWaitForCallback(
DownloadFileRenameMethodType method,
const base::FilePath& full_path,
base::FilePath* result_path_p) {
DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE); DownloadInterruptReason result_reason(DOWNLOAD_INTERRUPT_REASON_NONE);
bool callback_was_called(false);
base::FilePath result_path; base::FilePath result_path;
download_file_->RenameAndAnnotate( base::RunLoop loop_runner;
full_path, base::Bind(&DownloadFileTest::SetRenameResult, ((*download_file_).*method)(full_path,
weak_ptr_factory.GetWeakPtr(), base::Bind(&DownloadFileTest::SetRenameResult,
&callback_was_called, base::Unretained(this),
&result_reason, result_path_p)); loop_runner.QuitClosure(),
loop_.RunUntilIdle(); &result_reason,
result_path_p));
EXPECT_TRUE(callback_was_called); loop_runner.Run();
return result_reason; return result_reason;
} }
protected:
scoped_ptr<StrictMock<MockDownloadDestinationObserver> > observer_; scoped_ptr<StrictMock<MockDownloadDestinationObserver> > observer_;
base::WeakPtrFactory<DownloadDestinationObserver> observer_factory_; base::WeakPtrFactory<DownloadDestinationObserver> observer_factory_;
...@@ -300,17 +337,16 @@ class DownloadFileTest : public testing::Test { ...@@ -300,17 +337,16 @@ class DownloadFileTest : public testing::Test {
base::MessageLoop loop_; base::MessageLoop loop_;
private: private:
void SetRenameResult(bool* called_p, void SetRenameResult(const base::Closure& closure,
DownloadInterruptReason* reason_p, DownloadInterruptReason* reason_p,
base::FilePath* result_path_p, base::FilePath* result_path_p,
DownloadInterruptReason reason, DownloadInterruptReason reason,
const base::FilePath& result_path) { const base::FilePath& result_path) {
if (called_p)
*called_p = true;
if (reason_p) if (reason_p)
*reason_p = reason; *reason_p = reason;
if (result_path_p) if (result_path_p)
*result_path_p = result_path; *result_path_p = result_path;
closure.Run();
} }
// UI thread. // UI thread.
...@@ -322,6 +358,33 @@ class DownloadFileTest : public testing::Test { ...@@ -322,6 +358,33 @@ class DownloadFileTest : public testing::Test {
std::string expected_data_; 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 = const char* DownloadFileTest::kTestData1 =
"Let's write some data to the file!\n"; "Let's write some data to the file!\n";
const char* DownloadFileTest::kTestData2 = "Writing more data.\n"; const char* DownloadFileTest::kTestData2 = "Writing more data.\n";
...@@ -335,7 +398,7 @@ const int DownloadFileTest::kDummyRequestId = 67; ...@@ -335,7 +398,7 @@ const int DownloadFileTest::kDummyRequestId = 67;
// Rename the file before any data is downloaded, after some has, after it all // Rename the file before any data is downloaded, after some has, after it all
// has, and after it's closed. // has, and after it's closed.
TEST_F(DownloadFileTest, RenameFileFinal) { TEST_P(DownloadFileTestWithRename, RenameFileFinal) {
ASSERT_TRUE(CreateDownloadFile(0, true)); ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath()); base::FilePath initial_path(download_file_->FullPath());
EXPECT_TRUE(base::PathExists(initial_path)); EXPECT_TRUE(base::PathExists(initial_path));
...@@ -343,12 +406,11 @@ TEST_F(DownloadFileTest, RenameFileFinal) { ...@@ -343,12 +406,11 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
base::FilePath path_2(initial_path.InsertBeforeExtensionASCII("_2")); base::FilePath path_2(initial_path.InsertBeforeExtensionASCII("_2"));
base::FilePath path_3(initial_path.InsertBeforeExtensionASCII("_3")); base::FilePath path_3(initial_path.InsertBeforeExtensionASCII("_3"));
base::FilePath path_4(initial_path.InsertBeforeExtensionASCII("_4")); base::FilePath path_4(initial_path.InsertBeforeExtensionASCII("_4"));
base::FilePath path_5(initial_path.InsertBeforeExtensionASCII("_5"));
base::FilePath output_path; base::FilePath output_path;
// Rename the file before downloading any data. // Rename the file before downloading any data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndUniquify(path_1, &output_path)); InvokeSelectedRenameMethod(path_1, &output_path));
base::FilePath renamed_path = download_file_->FullPath(); base::FilePath renamed_path = download_file_->FullPath();
EXPECT_EQ(path_1, renamed_path); EXPECT_EQ(path_1, renamed_path);
EXPECT_EQ(path_1, output_path); EXPECT_EQ(path_1, output_path);
...@@ -363,7 +425,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) { ...@@ -363,7 +425,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading some data. // Rename the file after downloading some data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndUniquify(path_2, &output_path)); InvokeSelectedRenameMethod(path_2, &output_path));
renamed_path = download_file_->FullPath(); renamed_path = download_file_->FullPath();
EXPECT_EQ(path_2, renamed_path); EXPECT_EQ(path_2, renamed_path);
EXPECT_EQ(path_2, output_path); EXPECT_EQ(path_2, output_path);
...@@ -377,7 +439,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) { ...@@ -377,7 +439,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading all the data. // Rename the file after downloading all the data.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndUniquify(path_3, &output_path)); InvokeSelectedRenameMethod(path_3, &output_path));
renamed_path = download_file_->FullPath(); renamed_path = download_file_->FullPath();
EXPECT_EQ(path_3, renamed_path); EXPECT_EQ(path_3, renamed_path);
EXPECT_EQ(path_3, output_path); EXPECT_EQ(path_3, output_path);
...@@ -394,7 +456,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) { ...@@ -394,7 +456,7 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
// Rename the file after downloading all the data and closing the file. // Rename the file after downloading all the data and closing the file.
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndUniquify(path_4, &output_path)); InvokeSelectedRenameMethod(path_4, &output_path));
renamed_path = download_file_->FullPath(); renamed_path = download_file_->FullPath();
EXPECT_EQ(path_4, renamed_path); EXPECT_EQ(path_4, renamed_path);
EXPECT_EQ(path_4, output_path); EXPECT_EQ(path_4, output_path);
...@@ -407,29 +469,42 @@ TEST_F(DownloadFileTest, RenameFileFinal) { ...@@ -407,29 +469,42 @@ TEST_F(DownloadFileTest, RenameFileFinal) {
EXPECT_TRUE(download_file_->GetHash(&hash)); EXPECT_TRUE(download_file_->GetHash(&hash));
EXPECT_EQ(kDataHash, base::HexEncode(hash.data(), hash.size())); EXPECT_EQ(kDataHash, base::HexEncode(hash.data(), hash.size()));
// Check that a rename with overwrite to an existing file succeeds. DestroyDownloadFile(0);
std::string file_contents; }
ASSERT_FALSE(base::PathExists(path_5));
// 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"; static const char file_data[] = "xyzzy";
ASSERT_EQ(static_cast<int>(sizeof(file_data) - 1), ASSERT_EQ(static_cast<int>(sizeof(file_data)),
base::WriteFile(path_5, file_data, sizeof(file_data) - 1)); base::WriteFile(path_1, file_data, sizeof(file_data)));
ASSERT_TRUE(base::PathExists(path_5)); ASSERT_TRUE(base::PathExists(path_1));
EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents));
EXPECT_EQ(std::string(file_data), file_contents);
base::FilePath new_path;
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_NONE,
RenameAndAnnotate(path_5, &output_path)); RenameAndAnnotate(path_1, &new_path));
EXPECT_EQ(path_5, output_path); EXPECT_EQ(path_1.value(), new_path.value());
file_contents = ""; std::string file_contents;
EXPECT_TRUE(base::ReadFileToString(path_5, &file_contents)); ASSERT_TRUE(base::ReadFileToString(new_path, &file_contents));
EXPECT_NE(std::string(file_data), file_contents); EXPECT_NE(std::string(file_data), file_contents);
FinishStream(DOWNLOAD_INTERRUPT_REASON_NONE, true);
loop_.RunUntilIdle();
DestroyDownloadFile(0); DestroyDownloadFile(0);
} }
// Test to make sure the rename uniquifies if we aren't overwriting // 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) { TEST_F(DownloadFileTest, RenameUniquifies) {
ASSERT_TRUE(CreateDownloadFile(0, true)); ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath()); base::FilePath initial_path(download_file_->FullPath());
...@@ -451,16 +526,35 @@ TEST_F(DownloadFileTest, RenameUniquifies) { ...@@ -451,16 +526,35 @@ TEST_F(DownloadFileTest, RenameUniquifies) {
DestroyDownloadFile(0); 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 to make sure we get the proper error on failure.
TEST_F(DownloadFileTest, RenameError) { TEST_P(DownloadFileTestWithRename, RenameError) {
ASSERT_TRUE(CreateDownloadFile(0, true)); ASSERT_TRUE(CreateDownloadFile(0, true));
base::FilePath initial_path(download_file_->FullPath()); base::FilePath initial_path(download_file_->FullPath());
// Create a subdirectory. // Create a subdirectory.
base::FilePath tempdir( base::FilePath target_dir(
initial_path.DirName().Append(FILE_PATH_LITERAL("tempdir"))); initial_path.DirName().Append(FILE_PATH_LITERAL("TargetDir")));
ASSERT_TRUE(base::CreateDirectory(tempdir)); ASSERT_FALSE(base::DirectoryExists(target_dir));
base::FilePath target_path(tempdir.Append(initial_path.BaseName())); ASSERT_TRUE(base::CreateDirectory(target_dir));
base::FilePath target_path(target_dir.Append(initial_path.BaseName()));
// Targets // Targets
base::FilePath target_path_suffixed( base::FilePath target_path_suffixed(
...@@ -470,13 +564,13 @@ TEST_F(DownloadFileTest, RenameError) { ...@@ -470,13 +564,13 @@ TEST_F(DownloadFileTest, RenameError) {
// Make the directory unwritable and try to rename within it. // Make the directory unwritable and try to rename within it.
{ {
file_util::PermissionRestorer restorer(tempdir); file_util::PermissionRestorer restorer(target_dir);
ASSERT_TRUE(file_util::MakeFileUnwritable(tempdir)); ASSERT_TRUE(file_util::MakeFileUnwritable(target_dir));
// Expect nulling out of further processing. // Expect nulling out of further processing.
EXPECT_CALL(*input_stream_, RegisterCallback(IsNullCallback())); EXPECT_CALL(*input_stream_, RegisterCallback(IsNullCallback()));
EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED, EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
RenameAndAnnotate(target_path, NULL)); InvokeSelectedRenameMethod(target_path, NULL));
EXPECT_FALSE(base::PathExists(target_path_suffixed)); EXPECT_FALSE(base::PathExists(target_path_suffixed));
} }
...@@ -485,6 +579,98 @@ TEST_F(DownloadFileTest, RenameError) { ...@@ -485,6 +579,98 @@ TEST_F(DownloadFileTest, RenameError) {
DestroyDownloadFile(0); 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 file_util::PermissionRestorer 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.
file_util::PermissionRestorer restore_permissions_for(target_dir);
ASSERT_TRUE(file_util::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. // Various tests of the StreamActive method.
TEST_F(DownloadFileTest, StreamEmptySuccess) { TEST_F(DownloadFileTest, StreamEmptySuccess) {
ASSERT_TRUE(CreateDownloadFile(0, true)); ASSERT_TRUE(CreateDownloadFile(0, true));
......
...@@ -8,6 +8,35 @@ ...@@ -8,6 +8,35 @@
namespace content { 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( DownloadInterruptReason ConvertNetErrorToInterruptReason(
net::Error net_error, DownloadInterruptSource source) { net::Error net_error, DownloadInterruptSource source) {
switch (net_error) { switch (net_error) {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_ #ifndef CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_
#define 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 "content/public/browser/download_interrupt_reasons.h"
#include "net/base/net_errors.h" #include "net/base/net_errors.h"
...@@ -22,6 +23,10 @@ enum DownloadInterruptSource { ...@@ -22,6 +23,10 @@ enum DownloadInterruptSource {
DownloadInterruptReason CONTENT_EXPORT ConvertNetErrorToInterruptReason( DownloadInterruptReason CONTENT_EXPORT ConvertNetErrorToInterruptReason(
net::Error file_error, DownloadInterruptSource source); net::Error file_error, DownloadInterruptSource source);
// Safe to call from any thread.
DownloadInterruptReason CONTENT_EXPORT ConvertFileErrorToInterruptReason(
base::File::Error file_error);
} // namespace content } // namespace content
#endif // CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_ #endif // CONTENT_BROWSER_DOWNLOAD_DOWNLOAD_INTERRUPT_REASONS_IMPL_H_
...@@ -611,6 +611,18 @@ void RecordFileBandwidth(size_t length, ...@@ -611,6 +611,18 @@ void RecordFileBandwidth(size_t length,
disk_write_time_ms * 100 / elapsed_time_ms); 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) { void RecordSavePackageEvent(SavePackageEvent event) {
UMA_HISTOGRAM_ENUMERATION("Download.SavePackage", UMA_HISTOGRAM_ENUMERATION("Download.SavePackage",
event, event,
......
...@@ -200,6 +200,11 @@ void RecordFileBandwidth(size_t length, ...@@ -200,6 +200,11 @@ void RecordFileBandwidth(size_t length,
base::TimeDelta disk_write_time, base::TimeDelta disk_write_time,
base::TimeDelta elapsed_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 { enum SavePackageEvent {
// The user has started to save a page as a package. // The user has started to save a page as a package.
SAVE_PACKAGE_STARTED, SAVE_PACKAGE_STARTED,
......
...@@ -14,7 +14,6 @@ ...@@ -14,7 +14,6 @@
INTERRUPT_REASON(FILE_FAILED, 1) INTERRUPT_REASON(FILE_FAILED, 1)
// The file cannot be accessed due to security restrictions. // The file cannot be accessed due to security restrictions.
// The file cannot be accessed.
// "Access Denied". // "Access Denied".
INTERRUPT_REASON(FILE_ACCESS_DENIED, 2) INTERRUPT_REASON(FILE_ACCESS_DENIED, 2)
......
...@@ -4736,6 +4736,15 @@ Therefore, the affected-histogram name has to have at least one dot in it. ...@@ -4736,6 +4736,15 @@ Therefore, the affected-histogram name has to have at least one dot in it.
</summary> </summary>
</histogram> </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"> <histogram name="Download.OnChanged">
<owner>asanka@chromium.org</owner> <owner>asanka@chromium.org</owner>
<summary> <summary>
...@@ -4869,6 +4878,24 @@ Therefore, the affected-histogram name has to have at least one dot in it. ...@@ -4869,6 +4878,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> <summary>Time between the start of a download and its completion.</summary>
</histogram> </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"> <histogram name="Download.UserDiscard" enum="DownloadItem.DangerType">
<owner>asanka@chromium.org</owner> <owner>asanka@chromium.org</owner>
<owner>felt@chromium.org</owner> <owner>felt@chromium.org</owner>
...@@ -44086,7 +44113,9 @@ Therefore, the affected-histogram name has to have at least one dot in it. ...@@ -44086,7 +44113,9 @@ Therefore, the affected-histogram name has to have at least one dot in it.
<enum name="SpecialShFileOperationCodes" type="int"> <enum name="SpecialShFileOperationCodes" type="int">
<summary>Legacy error codes still returned by |ShFileOperation()|</summary> <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="113" label="Source and Destination are same file"/>
<int value="114" label="Multiple source mapped to single destination"/> <int value="114" label="Multiple source mapped to single destination"/>
<int value="115" label="Rename to different directory"/> <int value="115" label="Rename to different directory"/>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment