// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/installer/mini_installer/delete_with_retry.h"
#include <windows.h>
#include <utility>
namespace mini_installer {
namespace {
// A hook function (and accompanying context) for testing that is called when
// it's time to sleep before a retry.
SleepFunction g_hook_fn = nullptr;
void* g_hook_context = nullptr;
// Returns true if |error| is conceivably transient, and that it's reasonable to
// believe that retrying the operation could result in a different error.
bool IsTransientFailure(DWORD error) {
return
// ACCESS_DENIED could mean that the item has its read-only attribute set,
// that it is mapped into a process's address space, or that the item has
// been deleted but open handles remain.
error == ERROR_ACCESS_DENIED ||
// SHARING_VIOLATION generally means that there exists an open handle
// without SHARE_DELETE.
error == ERROR_SHARING_VIOLATION ||
// A directory is not considered empty until deletes of all items within
// it have been finalized.
error == ERROR_DIR_NOT_EMPTY;
}
// Marks the file or directory at |path| for deletion. Returns true if the file
// was present and was successfully marked, or false (populating |error| with a
// Windows error code) otherwise.
bool MarkForDeletion(const wchar_t* path, DWORD& error) {
// While it's tempting to use FILE_FLAG_DELETE_ON_CLOSE, doing so hides the
// success or failure of marking the file. Opening the file could fail if
// another process has it open without FILE_SHARE_DELETE.
HANDLE handle = ::CreateFileW(
path, DELETE, FILE_SHARE_DELETE | FILE_SHARE_WRITE | FILE_SHARE_READ,
/*lpSecurityAttributes=*/nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
/*hTemplateFile=*/nullptr);
if (handle == INVALID_HANDLE_VALUE) {
error = ::GetLastError();
return false; // Failed to open the file.
}
// Mark the file for deletion. On success, the file will be deleted when all
// open handles are closed. This could fail if another process has the file
// mapped into its address space, among other reasons.
FILE_DISPOSITION_INFO disposition = {/*DeleteFile=*/TRUE};
const bool succeeded =
::SetFileInformationByHandle(handle, FileDispositionInfo, &disposition,
sizeof(disposition)) != 0;
if (!succeeded)
error = ::GetLastError();
::CloseHandle(handle);
return succeeded;
}
// Clears the read-only attribute of |path|. Returns true if |path| was
// read-only and has had this attribute cleared, or false otherwise (e.g.,
// |path| names a file without the read-only attribute, |path| could not be
// opened).
bool ClearReadOnly(const wchar_t* path) {
HANDLE handle =
::CreateFileW(path, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
FILE_SHARE_DELETE | FILE_SHARE_WRITE | FILE_SHARE_READ,
/*lpSecurityAttributes=*/nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
/*hTemplateFile=*/nullptr);
if (handle == INVALID_HANDLE_VALUE)
return false; // Not modified.
bool modified = false;
FILE_BASIC_INFO info = {};
// Clear the read-only attribute if |path| names a file with it set.
if (::GetFileInformationByHandleEx(handle, FileBasicInfo, &info,
sizeof(info)) != 0 &&
(info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
modified = ::SetFileInformationByHandle(handle, FileBasicInfo, &info,
sizeof(info)) != 0;
}
::CloseHandle(handle);
return modified;
}
} // namespace
// This function does what we wish ::DeleteFile did, but obviously can't. It
// patiently waits for other processes to finish operating on the file/dir and
// only returns when the file/dir is truly gone. Since it blocks the calling
// thread for up to ten seconds, it is only suitable for specific cases like
// here in mini_installer where blocking is the desired behavior.
bool DeleteWithRetry(const wchar_t* path, int& attempts) {
constexpr DWORD kRetryPeriodMs = 100; // Wait 100ms between retries.
constexpr int kMaxAttempts = (10 * 1000) / kRetryPeriodMs; // Retry for 10s.
attempts = 1;
while (true) {
DWORD error;
if (MarkForDeletion(path, error)) {
// The item has been marked for deletion on close. It will not be deleted
// until all open handles across all processes have been closed. In the
// meantime, the name cannot be reused (attempts to create a new file with
// the same name will fail) and the containing directory cannot be deleted
// (it is not empty, after all). Before returning success to the caller,
// at least one more attempt to access the item must be made to determine
// whether or not deletion has taken place. This attempt is likely to
// return ACCESS_DENIED if handles remain open, FILE_NOT_FOUND if all
// handles have been closed, or even succss or SHARING_VIOLATION if the
// delete-on-close bit has been cleared through another open handle. To
// handle all of these cases, go back to the start of this loop to retry
// deletion immediately.
continue;
}
// The attempt to mark the file for deletion on close has failed.
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
return true; // Because the item doesn't exist -- the success case.
if (!IsTransientFailure(error))
return false; // There is no point in trying again.
// Try to clear the read-only attribute if there is probable cause.
if (error == ERROR_ACCESS_DENIED && ClearReadOnly(path))
continue; // Make another attempt to delete the file without delay.
if (attempts == kMaxAttempts)
break; // Enough is enough.
// Try again after letting other processes finish with the file/dir.
++attempts;
if (g_hook_fn)
(*g_hook_fn)(g_hook_context);
else
::Sleep(kRetryPeriodMs);
}
return false; // Failed all retries
}
SleepFunction SetRetrySleepHookForTesting(SleepFunction hook_fn,
void* hook_context) {
g_hook_context = hook_context;
return std::exchange(g_hook_fn, hook_fn);
}
} // namespace mini_installer