Skip to content
9 changes: 8 additions & 1 deletion Doc/library/fcntl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The module defines the following functions:
.. audit-event:: fcntl.flock fd,operation fcntl.flock


.. function:: lockf(fd, cmd, len=0, start=0, whence=0)
.. function:: lockf(fd, cmd, len=0, start=0, whence=0, /, open_file_descriptor=False)

This is essentially a wrapper around the :func:`~fcntl.fcntl` locking calls.
*fd* is the file descriptor (file objects providing a :meth:`~io.IOBase.fileno`
Expand Down Expand Up @@ -160,9 +160,16 @@ The module defines the following functions:
The default for *start* is 0, which means to start at the beginning of the file.
The default for *len* is 0 which means to lock to the end of the file. The
default for *whence* is also 0.
The default for *open_file_descriptor* is False, if *open_file_descriptor* is
set to True, open file description locks are used. Note that open file description
locks features are supported on Linux(>=3.15) otherwise it will raise :exc:`NotImplementedError`

.. audit-event:: fcntl.lockf fd,cmd,len,start,whence fcntl.lockf

.. versionchanged:: 3.9
The *open_file_descriptor* keyword argument was added.
ref: https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html

Examples (all on a SVR4 compliant system)::

import struct, fcntl, os
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ Added constants :data:`~fcntl.F_OFD_GETLK`, :data:`~fcntl.F_OFD_SETLK`
and :data:`~fcntl.F_OFD_SETLKW`.
(Contributed by Dong-hee Na in :issue:`38602`.)

Added an optional keyword argument *open_file_descriptor*
to :func:`fcntl.lockf` function.
(Contributed by Dong-hee Na in :issue:`22367`.)

ftplib
-------

Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_fcntl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
import struct
import sys
import threading
import time
import unittest
from multiprocessing import Process
from test.support import (verbose, TESTFN, unlink, run_unittest, import_module,
Expand Down Expand Up @@ -164,6 +166,43 @@ def test_lockf_exclusive(self):
fcntl.lockf(self.f, fcntl.LOCK_UN)
self.assertEqual(p.exitcode, 0)

@unittest.skipUnless(hasattr(fcntl, 'F_OFD_GETLK'), 'requires open file description locks')
def test_lockf_open_file_descriptor(self):
self.f = open(TESTFN, 'wb+')
fcntl.lockf(self.f, fcntl.LOCK_SH | fcntl.LOCK_NB, open_file_descriptor=True)
fcntl.lockf(self.f, fcntl.LOCK_UN, open_file_descriptor=True)
fcntl.lockf(self.f, fcntl.LOCK_EX | fcntl.LOCK_NB, open_file_descriptor=True)
fcntl.lockf(self.f, fcntl.LOCK_UN, open_file_descriptor=True)

@unittest.skipUnless(hasattr(fcntl, 'F_OFD_GETLK'), 'requires open file description locks')
def test_open_file_descriptor_example(self):
threads = []
num_threads = 3
num_iterations = 5
self.f = open(TESTFN, 'w+')
def thread_start(thread_id):
for i in range(num_iterations):
fcntl.lockf(self.f, fcntl.LOCK_EX, open_file_descriptor=True)
self.f.seek(0, os.SEEK_END)
self.f.write(f'{i}: tid={thread_id}, fd={self.f.fileno()}\n')
fcntl.lockf(self.f, fcntl.LOCK_UN, open_file_descriptor=True)
time.sleep(0.05)

for i in range(num_threads):
t = threading.Thread(target=thread_start, args=(i,))
threads.append(t)

for t in threads:
t.start()

for t in threads:
t.join()

self.f.close()
self.f = open(TESTFN, 'r')
res = self.f.readlines()
self.assertEqual(len(res), num_threads*num_iterations)

@unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError")
def test_lockf_share(self):
self.f = open(TESTFN, 'wb+')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add an optional keyword argument *open_file_descriptor* to
:func:`fcntl.lockf` function. Patch by Dong-hee Na.
44 changes: 32 additions & 12 deletions Modules/clinic/fcntlmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions Modules/fcntlmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ fcntl.lockf
start as startobj: object(c_default='NULL') = 0
whence: int = 0
/
open_file_descriptor: bool = False

A wrapper around the fcntl() locking calls.

Expand All @@ -375,12 +376,14 @@ starts. `whence` is as with fileobj.seek(), specifically:
0 - relative to the start of the file (SEEK_SET)
1 - relative to the current buffer position (SEEK_CUR)
2 - relative to the end of the file (SEEK_END)

if `open_file_descriptor` is set to True, open file description locks are used.
[clinic start generated code]*/

static PyObject *
fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj,
PyObject *startobj, int whence)
/*[clinic end generated code: output=4985e7a172e7461a input=3a5dc01b04371f1a]*/
PyObject *startobj, int whence, int open_file_descriptor)
/*[clinic end generated code: output=eba798e67a9aee86 input=83190651f560f35e]*/
{
int ret;
int async_err = 0;
Expand Down Expand Up @@ -435,7 +438,18 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj,
l.l_whence = whence;
do {
Py_BEGIN_ALLOW_THREADS
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
if (open_file_descriptor) {
#ifdef F_OFD_SETLK
l.l_pid = 0;
ret = fcntl(fd, (code & LOCK_NB) ? F_OFD_SETLK : F_OFD_SETLKW, &l);
#else
PyErr_SetString(PyExc_NotImplementedError,
"lockf: open_file_descriptor is not supported on this platform");
return NULL;
#endif
} else {
ret = fcntl(fd, (code & LOCK_NB) ? F_SETLK : F_SETLKW, &l);
}
Py_END_ALLOW_THREADS
} while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
}
Expand Down