Skip to content

Commit 183ea3e

Browse files
committed
Merge branch 'ps/mingw-rename'
The MinGW compatibility layer has been taught 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()`
2 parents 486c9d3 + 391bcea commit 183ea3e

File tree

2 files changed

+156
-9
lines changed

2 files changed

+156
-9
lines changed

compat/mingw.c

+151-6
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...)
502502
* to append to the file.
503503
*/
504504
handle = CreateFileW(wfilename, FILE_APPEND_DATA,
505-
FILE_SHARE_WRITE | FILE_SHARE_READ,
505+
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
506506
NULL, create, FILE_ATTRIBUTE_NORMAL, NULL);
507507
if (handle == INVALID_HANDLE_VALUE) {
508508
DWORD err = GetLastError();
@@ -532,6 +532,70 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...)
532532
return fd;
533533
}
534534

535+
/*
536+
* Ideally, we'd use `_wopen()` to implement this functionality so that we
537+
* don't have to reimplement it, but unfortunately we do not have tight control
538+
* over the share mode there. And while `_wsopen()` and friends exist that give
539+
* us _some_ control over the share mode, this family of functions doesn't give
540+
* us the ability to enable FILE_SHARE_DELETE, either. But this is a strict
541+
* requirement for us though so that we can unlink or rename over files that
542+
* are held open by another process.
543+
*
544+
* We are thus forced to implement our own emulation of `open()`. To make our
545+
* life simpler we only implement basic support for this, namely opening
546+
* existing files for reading and/or writing. This means that newly created
547+
* files won't have their sharing mode set up correctly, but for now I couldn't
548+
* find any case where this matters. We may have to revisit that in the future
549+
* though based on our needs.
550+
*/
551+
static int mingw_open_existing(const wchar_t *filename, int oflags, ...)
552+
{
553+
SECURITY_ATTRIBUTES security_attributes = {
554+
.nLength = sizeof(security_attributes),
555+
.bInheritHandle = !(oflags & O_NOINHERIT),
556+
};
557+
HANDLE handle;
558+
DWORD access;
559+
int fd;
560+
561+
/* We only support basic flags. */
562+
if (oflags & ~(O_ACCMODE | O_NOINHERIT)) {
563+
errno = ENOSYS;
564+
return -1;
565+
}
566+
567+
switch (oflags & O_ACCMODE) {
568+
case O_RDWR:
569+
access = GENERIC_READ | GENERIC_WRITE;
570+
break;
571+
case O_WRONLY:
572+
access = GENERIC_WRITE;
573+
break;
574+
default:
575+
access = GENERIC_READ;
576+
break;
577+
}
578+
579+
handle = CreateFileW(filename, access,
580+
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
581+
&security_attributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
582+
if (handle == INVALID_HANDLE_VALUE) {
583+
DWORD err = GetLastError();
584+
585+
/* See `mingw_open_append()` for why we have this conversion. */
586+
if (err == ERROR_INVALID_PARAMETER)
587+
err = ERROR_PATH_NOT_FOUND;
588+
589+
errno = err_win_to_posix(err);
590+
return -1;
591+
}
592+
593+
fd = _open_osfhandle((intptr_t)handle, oflags | O_BINARY);
594+
if (fd < 0)
595+
CloseHandle(handle);
596+
return fd;
597+
}
598+
535599
/*
536600
* Does the pathname map to the local named pipe filesystem?
537601
* That is, does it have a "//./pipe/" prefix?
@@ -567,6 +631,8 @@ int mingw_open (const char *filename, int oflags, ...)
567631

568632
if ((oflags & O_APPEND) && !is_local_named_pipe_path(filename))
569633
open_fn = mingw_open_append;
634+
else if (!(oflags & ~(O_ACCMODE | O_NOINHERIT)))
635+
open_fn = mingw_open_existing;
570636
else
571637
open_fn = _wopen;
572638

@@ -1006,7 +1072,7 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
10061072

10071073
osfilehandle = CreateFileW(wfilename,
10081074
FILE_WRITE_ATTRIBUTES,
1009-
0 /*FileShare.None*/,
1075+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
10101076
NULL,
10111077
OPEN_EXISTING,
10121078
(attrs != INVALID_FILE_ATTRIBUTES &&
@@ -2155,10 +2221,16 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)
21552221
#undef rename
21562222
int mingw_rename(const char *pold, const char *pnew)
21572223
{
2224+
static int supports_file_rename_info_ex = 1;
21582225
DWORD attrs, gle;
21592226
int tries = 0;
21602227
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2161-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2228+
int wpnew_len;
2229+
2230+
if (xutftowcs_path(wpold, pold) < 0)
2231+
return -1;
2232+
wpnew_len = xutftowcs_path(wpnew, pnew);
2233+
if (wpnew_len < 0)
21622234
return -1;
21632235

21642236
/*
@@ -2169,11 +2241,84 @@ int mingw_rename(const char *pold, const char *pnew)
21692241
return 0;
21702242
if (errno != EEXIST)
21712243
return -1;
2244+
21722245
repeat:
2173-
if (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))
2174-
return 0;
2246+
if (supports_file_rename_info_ex) {
2247+
/*
2248+
* Our minimum required Windows version is still set to Windows
2249+
* Vista. We thus have to declare required infrastructure for
2250+
* FileRenameInfoEx ourselves until we bump _WIN32_WINNT to
2251+
* 0x0A00. Furthermore, we have to handle cases where the
2252+
* FileRenameInfoEx call isn't supported yet.
2253+
*/
2254+
#define FILE_RENAME_FLAG_REPLACE_IF_EXISTS 0x00000001
2255+
#define FILE_RENAME_FLAG_POSIX_SEMANTICS 0x00000002
2256+
FILE_INFO_BY_HANDLE_CLASS FileRenameInfoEx = 22;
2257+
struct {
2258+
/*
2259+
* This is usually an unnamed union, but that is not
2260+
* part of ISO C99. We thus inline the field, as we
2261+
* really only care for the Flags field anyway.
2262+
*/
2263+
DWORD Flags;
2264+
HANDLE RootDirectory;
2265+
DWORD FileNameLength;
2266+
/*
2267+
* The actual structure is defined with a single-character
2268+
* flex array so that the structure has to be allocated on
2269+
* the heap. As we declare this structure ourselves though
2270+
* we can avoid the allocation and define FileName to have
2271+
* MAX_PATH bytes.
2272+
*/
2273+
WCHAR FileName[MAX_PATH];
2274+
} rename_info = { 0 };
2275+
HANDLE old_handle = INVALID_HANDLE_VALUE;
2276+
BOOL success;
2277+
2278+
old_handle = CreateFileW(wpold, DELETE,
2279+
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE,
2280+
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
2281+
if (old_handle == INVALID_HANDLE_VALUE) {
2282+
errno = err_win_to_posix(GetLastError());
2283+
return -1;
2284+
}
2285+
2286+
rename_info.Flags = FILE_RENAME_FLAG_REPLACE_IF_EXISTS |
2287+
FILE_RENAME_FLAG_POSIX_SEMANTICS;
2288+
rename_info.FileNameLength = wpnew_len * sizeof(WCHAR);
2289+
memcpy(rename_info.FileName, wpnew, wpnew_len * sizeof(WCHAR));
2290+
2291+
success = SetFileInformationByHandle(old_handle, FileRenameInfoEx,
2292+
&rename_info, sizeof(rename_info));
2293+
gle = GetLastError();
2294+
CloseHandle(old_handle);
2295+
if (success)
2296+
return 0;
2297+
2298+
/*
2299+
* When we see ERROR_INVALID_PARAMETER we can assume that the
2300+
* current system doesn't support FileRenameInfoEx. Keep us
2301+
* from using it in future calls and retry.
2302+
*/
2303+
if (gle == ERROR_INVALID_PARAMETER) {
2304+
supports_file_rename_info_ex = 0;
2305+
goto repeat;
2306+
}
2307+
2308+
/*
2309+
* In theory, we shouldn't get ERROR_ACCESS_DENIED because we
2310+
* always open files with FILE_SHARE_DELETE But in practice we
2311+
* cannot assume that Git is the only one accessing files, and
2312+
* other applications may not set FILE_SHARE_DELETE. So we have
2313+
* to retry.
2314+
*/
2315+
} else {
2316+
if (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))
2317+
return 0;
2318+
gle = GetLastError();
2319+
}
2320+
21752321
/* TODO: translate more errors */
2176-
gle = GetLastError();
21772322
if (gle == ERROR_ACCESS_DENIED &&
21782323
(attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {
21792324
if (attrs & FILE_ATTRIBUTE_DIRECTORY) {

t/t0610-reftable-basics.sh

+5-3
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,12 @@ test_expect_success 'ref transaction: retry acquiring tables.list lock' '
450450
)
451451
'
452452

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

0 commit comments

Comments
 (0)