1
0
mirror of https://github.com/skeeto/w64devkit.git synced 2026-10-23 06:08:56 +03:00

Make flock exclude other locks on Windows

busybox-w32 locked the file's bytes from 0 to its current size, so on
an empty lock file, the usual case, flock locked nothing, and on a
non-empty file Windows' mandatory locks kept even the locked command
from reading or writing it. Lock one byte far past the end of the file
instead, which behaves like flock(2). Also let flock -n fail silently
and flock -u succeed, as on Unix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christopher Wellons
2026-10-22 21:08:02 -04:00
parent 081983e3ad
commit c13118e28d
3 changed files with 143 additions and 46 deletions

View File

@@ -37,9 +37,7 @@ If
.I file
does not exist,
.B flock
creates it as an empty file; see
.B Lock range
below.
creates it as an empty file.
.I file
is opened for reading only, and
.B flock
@@ -106,32 +104,22 @@ shares the standard input, output, and error of
.BR flock ,
which writes nothing itself except error messages on standard error.
.SS Lock range
Windows locks ranges of bytes in a file rather than whole files, and its
locks are mandatory: no other process can read or write the bytes that a
lock covers.
.B flock
locks the bytes of
.I file
that exist when the lock is taken, from offset 0 up to the size of the
file at that moment.
Bytes appended later are not covered.
A lock on an empty file covers no bytes, and Windows does not let a lock
of zero bytes conflict with any other lock.
A lock on an empty file therefore excludes nothing, and two
therefore locks a single byte at offset 2^62, past the end of any real
file.
That lock conflicts with other
.B flock
commands on it run their programs at the same time.
Write at least one byte to a lock file before using it, as in
.BR EXAMPLES .
.PP
Windows locks are mandatory, not advisory.
While
.B flock
holds an exclusive lock, no other process, including
locks on the same file, but does not stop any process, including
.IR prog ,
can read or write the locked bytes of
.IR file .
While it holds a shared lock, no process can write them.
Such a read or write fails with a permission error.
Use a separate file as the lock, not the data that
.I prog
works on.
from reading or writing
.IR file ,
so the lock is advisory, as on Unix.
An empty file works as a lock file.
This is a w64devkit change; see
.BR NOTES .
.SH OPTIONS
Options are recognized only before
.I file
@@ -173,13 +161,14 @@ A lock belongs to the process that took it, and a new
.B flock
process holds none, so
.B \-u
always fails with status 1, and
releases nothing.
It succeeds, as on Unix, and then runs
.I prog
does not run.
if one is given.
.TP
.BR \-n ", " \-\-nonblock
Do not wait for a conflicting lock to be released.
Exit at once with status 1 and an error message instead.
Exit at once with status 1 instead, with no message.
.TP
\fB\-c\fR, \fB\-\-command\fR \fIcommand\fR
Run
@@ -275,9 +264,6 @@ flock: \fIprog\fR: \fIreason\fR
.PP
The second form, with an empty field before the reason, reports a
failure to lock or unlock, such as
.B Permission denied
for a conflicting lock with
.BR \-n ,
.B Invalid argument
for
.B \-s
@@ -289,12 +275,10 @@ for an
.I fd
that is not open.
.SH EXAMPLES
Create a lock file of one line, then run a build that no other build
using the same lock file can overlap:
Run a build that no other build using the same lock file can overlap:
.PP
.RS 4
.EX
$ echo lock >build.lock
$ flock build.lock make
.EE
.RE
@@ -336,17 +320,6 @@ it.
The only use of this form is to wait until no other process holds a
conflicting lock.
.IP \(bu 2
.B \-u
always fails, as described under
.BR OPTIONS .
.IP \(bu 2
Locks are mandatory, and a lock on an empty file excludes nothing; see
.BR "Lock range" .
.IP \(bu 2
With
.BR \-n ,
a conflicting lock gives an error message as well as status 1.
.IP \(bu 2
A directory cannot be opened, so it cannot be locked.
.B flock
exits with status 1 and
@@ -365,6 +338,21 @@ Windows does not let another process delete or rename
while
.B flock
has it open.
.PP
w64devkit changes the lock range, as described under
.BR "Lock range" .
busybox-w32 locks the bytes of the file that exist when the lock is
taken, so on an empty file it locks nothing and excludes no one, and on
a non-empty file it stops other processes, including
.IR prog ,
from using those bytes.
w64devkit also makes
.B \-n
fail silently and
.B \-u
succeed, as on Unix, where busybox-w32 prints
.B Permission denied
or fails.
.SH SEE ALSO
.BR busybox (1),
.BR sh (1),

View File

@@ -0,0 +1,108 @@
Fix flock on Windows, emulated with LockFileEx, so that it excludes
other flock users the way flock(2) does:
* It locked the byte range from 0 to the file's current size, so on a
new, empty lock file, the usual case, it locked nothing at all, and
"flock lockfile cmd" excluded no one.
* Since Windows byte-range locks are mandatory, on a non-empty file it
stopped every other process, including the command run under the
lock, from reading or writing the locked bytes.
Lock one byte at offset 2^62 instead, past the end of any real file,
which other locks respect and ordinary I/O does not touch.
Also report a lock held elsewhere as EWOULDBLOCK rather than EACCES,
so that flock -n exits with status 1 silently instead of printing
"Permission denied", and let unlocking a file that holds no lock
succeed, as flock(2) does, rather than failing flock -u.
--- a/win32/flock.c
+++ b/win32/flock.c
@@ -31,59 +31,45 @@
/* _get_osfhandle */
# include <io.h>
-/* Determine the current size of a file. Because the other braindead
- * APIs we'll call need lower/upper 32 bit pairs, keep the file size
- * like that too.
- */
-static BOOL
-file_size (HANDLE h, DWORD * lower, DWORD * upper)
-{
- *lower = GetFileSize (h, upper);
- return 1;
-}
-
/* LOCKFILE_FAIL_IMMEDIATELY is undefined on some Windows systems. */
# ifndef LOCKFILE_FAIL_IMMEDIATELY
# define LOCKFILE_FAIL_IMMEDIATELY 1
# endif
+/* Lock a single byte far beyond the end of any real file rather than
+ * the file's contents. Locking the contents locked nothing when the
+ * file was empty, as a new lock file is, and since Windows locks are
+ * mandatory, it stopped every other process, including the command
+ * run under the lock, from reading or writing the file. A lock past
+ * the end of the file excludes only other locks, like flock(2).
+ */
+#define LOCK_OFFSET_HIGH 0x40000000 /* offset 2^62 */
+
/* Acquire a lock. */
static BOOL
do_lock (HANDLE h, int non_blocking, int exclusive)
{
- BOOL res;
- DWORD size_lower, size_upper;
OVERLAPPED ovlp;
int flags = 0;
- /* We're going to lock the whole file, so get the file size. */
- res = file_size (h, &size_lower, &size_upper);
- if (!res)
- return 0;
-
- /* Start offset is 0, and also zero the remaining members of this struct. */
memset (&ovlp, 0, sizeof ovlp);
+ ovlp.OffsetHigh = LOCK_OFFSET_HIGH;
if (non_blocking)
flags |= LOCKFILE_FAIL_IMMEDIATELY;
if (exclusive)
flags |= LOCKFILE_EXCLUSIVE_LOCK;
- return LockFileEx (h, flags, 0, size_lower, size_upper, &ovlp);
+ return LockFileEx (h, flags, 0, 1, 0, &ovlp);
}
/* Unlock reader or exclusive lock. */
static BOOL
do_unlock (HANDLE h)
{
- int res;
- DWORD size_lower, size_upper;
-
- res = file_size (h, &size_lower, &size_upper);
- if (!res)
- return 0;
-
- return UnlockFile (h, 0, 0, size_lower, size_upper);
+ /* Unlocking a file that holds no lock succeeds, as in flock(2). */
+ return UnlockFile (h, 0, LOCK_OFFSET_HIGH, 1, 0) ||
+ GetLastError () == ERROR_NOT_LOCKED;
}
/* Now our BSD-like flock operation. */
@@ -124,7 +110,11 @@
*/
if (!res)
{
- errno = err_win_to_posix();
+ /* A lock held elsewhere is EWOULDBLOCK, as flock(2) reports. */
+ if (GetLastError () == ERROR_LOCK_VIOLATION)
+ errno = EWOULDBLOCK;
+ else
+ errno = err_win_to_posix();
return -1;
}

View File

@@ -12,3 +12,4 @@ bare-drive.patch
rm-bare-drive.patch
drop-test2.patch
stty-winsize.patch
flock-range.patch