Merge branch 'ps/mingw-rename' into next

Teaches the MinGW compatibility layer to support POSIX semantics for
atomic renames when other process(es) have a file opened at the
destination path.

* ps/mingw-rename:
  compat/mingw: support POSIX semantics for atomic renames
  compat/mingw: allow deletion of most opened files
  compat/mingw: share file handles created via `CreateFileW()`
This commit is contained in:
Junio C Hamano 2024-11-06 00:32:49 -08:00
commit 6dd2fffec7
2 changed files with 156 additions and 9 deletions

View File

@ -502,7 +502,7 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...)
* to append to the file. * to append to the file.
*/ */
handle = CreateFileW(wfilename, FILE_APPEND_DATA, handle = CreateFileW(wfilename, FILE_APPEND_DATA,
FILE_SHARE_WRITE | FILE_SHARE_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
NULL, create, FILE_ATTRIBUTE_NORMAL, NULL); NULL, create, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE) { if (handle == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError(); DWORD err = GetLastError();
@ -532,6 +532,70 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...)
return fd; return fd;
} }
/*
* Ideally, we'd use `_wopen()` to implement this functionality so that we
* don't have to reimplement it, but unfortunately we do not have tight control
* over the share mode there. And while `_wsopen()` and friends exist that give
* us _some_ control over the share mode, this family of functions doesn't give
* us the ability to enable FILE_SHARE_DELETE, either. But this is a strict
* requirement for us though so that we can unlink or rename over files that
* are held open by another process.
*
* We are thus forced to implement our own emulation of `open()`. To make our
* life simpler we only implement basic support for this, namely opening
* existing files for reading and/or writing. This means that newly created
* files won't have their sharing mode set up correctly, but for now I couldn't
* find any case where this matters. We may have to revisit that in the future
* though based on our needs.
*/
static int mingw_open_existing(const wchar_t *filename, int oflags, ...)
{
SECURITY_ATTRIBUTES security_attributes = {
.nLength = sizeof(security_attributes),
.bInheritHandle = !(oflags & O_NOINHERIT),
};
HANDLE handle;
DWORD access;
int fd;
/* We only support basic flags. */
if (oflags & ~(O_ACCMODE | O_NOINHERIT)) {
errno = ENOSYS;
return -1;
}
switch (oflags & O_ACCMODE) {
case O_RDWR:
access = GENERIC_READ | GENERIC_WRITE;
break;
case O_WRONLY:
access = GENERIC_WRITE;
break;
default:
access = GENERIC_READ;
break;
}
handle = CreateFileW(filename, access,
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
&security_attributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
/* See `mingw_open_append()` for why we have this conversion. */
if (err == ERROR_INVALID_PARAMETER)
err = ERROR_PATH_NOT_FOUND;
errno = err_win_to_posix(err);
return -1;
}
fd = _open_osfhandle((intptr_t)handle, oflags | O_BINARY);
if (fd < 0)
CloseHandle(handle);
return fd;
}
/* /*
* Does the pathname map to the local named pipe filesystem? * Does the pathname map to the local named pipe filesystem?
* That is, does it have a "//./pipe/" prefix? * That is, does it have a "//./pipe/" prefix?
@ -567,6 +631,8 @@ int mingw_open (const char *filename, int oflags, ...)
if ((oflags & O_APPEND) && !is_local_named_pipe_path(filename)) if ((oflags & O_APPEND) && !is_local_named_pipe_path(filename))
open_fn = mingw_open_append; open_fn = mingw_open_append;
else if (!(oflags & ~(O_ACCMODE | O_NOINHERIT)))
open_fn = mingw_open_existing;
else else
open_fn = _wopen; open_fn = _wopen;
@ -1006,7 +1072,7 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
osfilehandle = CreateFileW(wfilename, osfilehandle = CreateFileW(wfilename,
FILE_WRITE_ATTRIBUTES, FILE_WRITE_ATTRIBUTES,
0 /*FileShare.None*/, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, NULL,
OPEN_EXISTING, OPEN_EXISTING,
(attrs != INVALID_FILE_ATTRIBUTES && (attrs != INVALID_FILE_ATTRIBUTES &&
@ -2155,10 +2221,16 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)
#undef rename #undef rename
int mingw_rename(const char *pold, const char *pnew) int mingw_rename(const char *pold, const char *pnew)
{ {
static int supports_file_rename_info_ex = 1;
DWORD attrs, gle; DWORD attrs, gle;
int tries = 0; int tries = 0;
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH]; wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0) int wpnew_len;
if (xutftowcs_path(wpold, pold) < 0)
return -1;
wpnew_len = xutftowcs_path(wpnew, pnew);
if (wpnew_len < 0)
return -1; return -1;
/* /*
@ -2169,11 +2241,84 @@ int mingw_rename(const char *pold, const char *pnew)
return 0; return 0;
if (errno != EEXIST) if (errno != EEXIST)
return -1; return -1;
repeat: repeat:
if (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING)) if (supports_file_rename_info_ex) {
return 0; /*
* Our minimum required Windows version is still set to Windows
* Vista. We thus have to declare required infrastructure for
* FileRenameInfoEx ourselves until we bump _WIN32_WINNT to
* 0x0A00. Furthermore, we have to handle cases where the
* FileRenameInfoEx call isn't supported yet.
*/
#define FILE_RENAME_FLAG_REPLACE_IF_EXISTS 0x00000001
#define FILE_RENAME_FLAG_POSIX_SEMANTICS 0x00000002
FILE_INFO_BY_HANDLE_CLASS FileRenameInfoEx = 22;
struct {
/*
* This is usually an unnamed union, but that is not
* part of ISO C99. We thus inline the field, as we
* really only care for the Flags field anyway.
*/
DWORD Flags;
HANDLE RootDirectory;
DWORD FileNameLength;
/*
* The actual structure is defined with a single-character
* flex array so that the structure has to be allocated on
* the heap. As we declare this structure ourselves though
* we can avoid the allocation and define FileName to have
* MAX_PATH bytes.
*/
WCHAR FileName[MAX_PATH];
} rename_info = { 0 };
HANDLE old_handle = INVALID_HANDLE_VALUE;
BOOL success;
old_handle = CreateFileW(wpold, DELETE,
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (old_handle == INVALID_HANDLE_VALUE) {
errno = err_win_to_posix(GetLastError());
return -1;
}
rename_info.Flags = FILE_RENAME_FLAG_REPLACE_IF_EXISTS |
FILE_RENAME_FLAG_POSIX_SEMANTICS;
rename_info.FileNameLength = wpnew_len * sizeof(WCHAR);
memcpy(rename_info.FileName, wpnew, wpnew_len * sizeof(WCHAR));
success = SetFileInformationByHandle(old_handle, FileRenameInfoEx,
&rename_info, sizeof(rename_info));
gle = GetLastError();
CloseHandle(old_handle);
if (success)
return 0;
/*
* When we see ERROR_INVALID_PARAMETER we can assume that the
* current system doesn't support FileRenameInfoEx. Keep us
* from using it in future calls and retry.
*/
if (gle == ERROR_INVALID_PARAMETER) {
supports_file_rename_info_ex = 0;
goto repeat;
}
/*
* In theory, we shouldn't get ERROR_ACCESS_DENIED because we
* always open files with FILE_SHARE_DELETE But in practice we
* cannot assume that Git is the only one accessing files, and
* other applications may not set FILE_SHARE_DELETE. So we have
* to retry.
*/
} else {
if (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))
return 0;
gle = GetLastError();
}
/* TODO: translate more errors */ /* TODO: translate more errors */
gle = GetLastError();
if (gle == ERROR_ACCESS_DENIED && if (gle == ERROR_ACCESS_DENIED &&
(attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) { (attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {
if (attrs & FILE_ATTRIBUTE_DIRECTORY) { if (attrs & FILE_ATTRIBUTE_DIRECTORY) {

View File

@ -450,10 +450,12 @@ test_expect_success 'ref transaction: retry acquiring tables.list lock' '
) )
' '
# This test fails most of the time on Windows systems. The root cause is # This test fails most of the time on Cygwin systems. The root cause is
# that Windows does not allow us to rename the "tables.list.lock" file into # that Windows does not allow us to rename the "tables.list.lock" file into
# place when "tables.list" is open for reading by a concurrent process. # place when "tables.list" is open for reading by a concurrent process. We have
test_expect_success !WINDOWS 'ref transaction: many concurrent writers' ' # worked around that in our MinGW-based rename emulation, but the Cygwin
# emulation seems to be insufficient.
test_expect_success !CYGWIN 'ref transaction: many concurrent writers' '
test_when_finished "rm -rf repo" && test_when_finished "rm -rf repo" &&
git init repo && git init repo &&
( (