diff --git a/Doc/library/fcntl.rst b/Doc/library/fcntl.rst index 7bd64e43dd5bfe..cd2179c5679950 100644 --- a/Doc/library/fcntl.rst +++ b/Doc/library/fcntl.rst @@ -214,6 +214,38 @@ The module defines the following functions: .. audit-event:: fcntl.lockf fd,cmd,len,start,whence fcntl.lockf +.. function:: getlk(fd, cmd, len=0, start=0, whence=0) + + Like :func:`lockf`, essentially a wrapper for the ``F_GETLK`` lock operation. + + *fd* is the file descriptor (file objects providing a :meth:`~io.IOBase.fileno` + method are accepted as well) of the file to retrieve lock information on, + and *cmd* specifies an operation to check for would-be conflicts on: + + * :const:`LOCK_SH` -- check for conflicting exclusive lock + * :const:`LOCK_EX` -- check for conflicting shared or exclusive lock + + Note that the returned information identifies a lock that would conflict + with acquiring the type of lock specified in *cmd*. Calling with + :const:`LOCK_SH` therefore only returns information on a conflicting + :const:`LOCK_EX`, while calling with :const:`LOCK_EX` returns information + on either :const:`LOCK_SH` or :const:`LOCK_EX` locks. + + The *len*, *start* and *whence* parameters are as with :func:`lockf`. + + Returns a tuple ``(pid, cmd, len, start, whence)`` if a conflicting lock is + found. If more than one lock conflicts, platform code chooses one. There + may not be an associated process, in which case *pid* will be -1. + + If no other lock would conflict with the request, this function returns + ``None``. + + The information returned by this call may already be outdated by the time it + returns. Additionally, only locks acquired using :func:`lockf` (or rather + the underlying ``fcntl(F_SETLK)`` system call) are considered by this call. + + .. audit-event:: fcntl.getlk fd,cmd,len,start,whence fcntl.getlk + Examples (all on a SVR4 compliant system):: import struct, fcntl, os diff --git a/Lib/test/test_fcntl.py b/Lib/test/test_fcntl.py index bb784cbe2ce036..cb4761cc8cbf6e 100644 --- a/Lib/test/test_fcntl.py +++ b/Lib/test/test_fcntl.py @@ -27,18 +27,29 @@ def fileno(self): def try_lockf_on_other_process_fail(fname, cmd): f = open(fname, 'wb+') try: + lockinfo = fcntl.getlk(f, cmd & ~fcntl.LOCK_NB) fcntl.lockf(f, cmd) except BlockingIOError: pass finally: f.close() + ppid = os.getppid() + if lockinfo is None or lockinfo[0] not in {ppid, -1}: + sys.stderr.write(f"getlk: {lockinfo}, expected pid={ppid}\n") + sys.exit(1) + def try_lockf_on_other_process(fname, cmd): f = open(fname, 'wb+') fcntl.lockf(f, cmd) fcntl.lockf(f, fcntl.LOCK_UN) + lockinfo = fcntl.getlk(f, cmd & ~fcntl.LOCK_NB) f.close() + if lockinfo is not None: + sys.stderr.write(f"getlk: {lockinfo}, expected: None\n") + sys.exit(1) + class TestFcntl(unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Library/2022-09-08-23-35-52.gh-issue-96694.p-dgqF.rst b/Misc/NEWS.d/next/Library/2022-09-08-23-35-52.gh-issue-96694.p-dgqF.rst new file mode 100644 index 00000000000000..d1d2d35139a1d1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-09-08-23-35-52.gh-issue-96694.p-dgqF.rst @@ -0,0 +1,2 @@ +The ``fcntl`` module has a new ``getlk`` wrapper to retrieve information on +conflicting locks through the fcntl(F_GETLK) call. diff --git a/Modules/clinic/fcntlmodule.c.h b/Modules/clinic/fcntlmodule.c.h index 53b139e09afdf1..a3246525d2c6b9 100644 --- a/Modules/clinic/fcntlmodule.c.h +++ b/Modules/clinic/fcntlmodule.c.h @@ -264,4 +264,88 @@ fcntl_lockf(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=45a56f53fd17ff3c input=a9049054013a1b77]*/ + +PyDoc_STRVAR(fcntl_getlk__doc__, +"getlk($module, fd, cmd, len=0, start=0, whence=0, /)\n" +"--\n" +"\n" +"A wrapper around the fcntl(F_GETLK) locking call.\n" +"\n" +"`fd` is the file descriptor of the file whose lock status to get. `cmd`\n" +"is one of the following values:\n" +"\n" +" LOCK_SH - check for conflicting exclusive lock\n" +" LOCK_EX - check for conflicting shared or exclusive lock\n" +"\n" +"Note that the returned information identifies a lock that would conflict with\n" +"acquiring the type of lock specified in `cmd`. Calling with LOCK_SH therefore\n" +"only returns information on a conflicting LOCK_EX, while calling with LOCK_EX\n" +"returns information on either LOCK_SH or LOCK_EX locks.\n" +"\n" +"The remaining parameters are as with `lockf`.\n" +"\n" +"Returns a tuple of (pid, cmd, len, start, whence) if a conflicting lock is\n" +"found. If more than one lock conflicts, platform code chooses one. There\n" +"may not be an associated process, in which case pid will be -1.\n" +"\n" +"If no other lock would conflict with the request, this function returns None.\n" +"\n" +"The information returned by this call may already be outdated by the time it\n" +"returns. Additionally, only locks acquired using `lockf` (or `fcntl(F_SETLK)`)\n" +"are considered by this call."); + +#define FCNTL_GETLK_METHODDEF \ + {"getlk", (PyCFunction)(void(*)(void))fcntl_getlk, METH_FASTCALL, fcntl_getlk__doc__}, + +static PyObject * +fcntl_getlk_impl(PyObject *module, int fd, int code, PyObject *lenobj, + PyObject *startobj, int whence); + +static PyObject * +fcntl_getlk(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int fd; + int code; + PyObject *lenobj = NULL; + PyObject *startobj = NULL; + int whence = 0; + + if (nargs < 2) { + PyErr_Format(PyExc_TypeError, "getlk expected at least 2 arguments, got %zd", nargs); + goto exit; + } + if (nargs > 5) { + PyErr_Format(PyExc_TypeError, "getlk expected at most 5 arguments, got %zd", nargs); + goto exit; + } + fd = PyObject_AsFileDescriptor(args[0]); + if (fd < 0) { + goto exit; + } + code = PyLong_AsInt(args[1]); + if (code == -1 && PyErr_Occurred()) { + goto exit; + } + if (nargs < 3) { + goto skip_optional; + } + lenobj = args[2]; + if (nargs < 4) { + goto skip_optional; + } + startobj = args[3]; + if (nargs < 5) { + goto skip_optional; + } + whence = PyLong_AsInt(args[4]); + if (whence == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = fcntl_getlk_impl(module, fd, code, lenobj, startobj, whence); + +exit: + return return_value; +} +/*[clinic end generated code: output=c81edf8fa810b9c1 input=a9049054013a1b77]*/ diff --git a/Modules/fcntlmodule.c b/Modules/fcntlmodule.c index 90ebfd7e99a777..7c6532c93daa46 100644 --- a/Modules/fcntlmodule.c +++ b/Modules/fcntlmodule.c @@ -339,6 +339,30 @@ fcntl_flock_impl(PyObject *module, int fd, int code) } +static void +fill_struct_flock(struct flock *l, PyObject *lenobj, PyObject *startobj) +{ + l->l_start = l->l_len = 0; + if (startobj != NULL) { +#if !defined(HAVE_LARGEFILE_SUPPORT) + l->l_start = PyLong_AsLong(startobj); +#else + l->l_start = PyLong_Check(startobj) ? + PyLong_AsLongLong(startobj) : + PyLong_AsLong(startobj); +#endif + } + if (lenobj != NULL) { +#if !defined(HAVE_LARGEFILE_SUPPORT) + l->l_len = PyLong_AsLong(lenobj); +#else + l->l_len = PyLong_Check(lenobj) ? + PyLong_AsLongLong(lenobj) : + PyLong_AsLong(lenobj); +#endif + } +} + /*[clinic input] fcntl.lockf @@ -405,29 +429,9 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj, "unrecognized lockf argument"); return NULL; } - l.l_start = l.l_len = 0; - if (startobj != NULL) { -#if !defined(HAVE_LARGEFILE_SUPPORT) - l.l_start = PyLong_AsLong(startobj); -#else - l.l_start = PyLong_Check(startobj) ? - PyLong_AsLongLong(startobj) : - PyLong_AsLong(startobj); -#endif - if (PyErr_Occurred()) - return NULL; - } - if (lenobj != NULL) { -#if !defined(HAVE_LARGEFILE_SUPPORT) - l.l_len = PyLong_AsLong(lenobj); -#else - l.l_len = PyLong_Check(lenobj) ? - PyLong_AsLongLong(lenobj) : - PyLong_AsLong(lenobj); -#endif - if (PyErr_Occurred()) - return NULL; - } + fill_struct_flock(&l, lenobj, startobj); + if (PyErr_Occurred()) + return NULL; l.l_whence = whence; do { Py_BEGIN_ALLOW_THREADS @@ -441,6 +445,96 @@ fcntl_lockf_impl(PyObject *module, int fd, int code, PyObject *lenobj, Py_RETURN_NONE; } + +/*[clinic input] +fcntl.getlk + + fd: fildes + cmd as code: int + len as lenobj: object(c_default='NULL') = 0 + start as startobj: object(c_default='NULL') = 0 + whence: int = 0 + / + +A wrapper around the fcntl(F_GETLK) locking call. + +`fd` is the file descriptor of the file whose lock status to get. `cmd` +is one of the following values: + + LOCK_SH - check for conflicting exclusive lock + LOCK_EX - check for conflicting shared or exclusive lock + +Note that the returned information identifies a lock that would conflict with +acquiring the type of lock specified in `cmd`. Calling with LOCK_SH therefore +only returns information on a conflicting LOCK_EX, while calling with LOCK_EX +returns information on either LOCK_SH or LOCK_EX locks. + +The remaining parameters are as with `lockf`. + +Returns a tuple of (pid, cmd, len, start, whence) if a conflicting lock is +found. If more than one lock conflicts, platform code chooses one. There +may not be an associated process, in which case pid will be -1. + +If no other lock would conflict with the request, this function returns None. + +The information returned by this call may already be outdated by the time it +returns. Additionally, only locks acquired using `lockf` (or `fcntl(F_SETLK)`) +are considered by this call. +[clinic start generated code]*/ + +static PyObject * +fcntl_getlk_impl(PyObject *module, int fd, int code, PyObject *lenobj, + PyObject *startobj, int whence) +/*[clinic end generated code: output=2a7ba40514c0f66b input=336315433d0b46b2]*/ +{ + int ret; + int async_err = 0; + struct flock l; + + if (PySys_Audit("fcntl.getlk", "iiOOi", fd, code, lenobj ? lenobj : Py_None, + startobj ? startobj : Py_None, whence) < 0) { + return NULL; + } + + /* LOCK_UN and combinations make no sense for F_GETLK */ + if (code == LOCK_SH) + l.l_type = F_RDLCK; + else if (code == LOCK_EX) + l.l_type = F_WRLCK; + else { + PyErr_SetString(PyExc_ValueError, + "unrecognized getlk argument"); + return NULL; + } + fill_struct_flock(&l, lenobj, startobj); + if (PyErr_Occurred()) + return NULL; + l.l_whence = whence; + do { + Py_BEGIN_ALLOW_THREADS + ret = fcntl(fd, F_GETLK, &l); + Py_END_ALLOW_THREADS + } while (ret == -1 && errno == EINTR && !(async_err = PyErr_CheckSignals())); + + if (ret < 0) { + return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL; + } else { + int cmd = 0; + + if (l.l_type == F_UNLCK) + Py_RETURN_NONE; + else if (l.l_type == F_RDLCK) + cmd = LOCK_SH; + else if (l.l_type == F_WRLCK) + cmd = LOCK_EX; + + /* casts necessary since types in OS header are unknown */ + return Py_BuildValue("LiLLi", (long long)l.l_pid, cmd, + (long long)l.l_len, (long long)l.l_start, + (int)l.l_whence); + } +} + /* List of functions */ static PyMethodDef fcntl_methods[] = { @@ -448,6 +542,7 @@ static PyMethodDef fcntl_methods[] = { FCNTL_IOCTL_METHODDEF FCNTL_FLOCK_METHODDEF FCNTL_LOCKF_METHODDEF + FCNTL_GETLK_METHODDEF {NULL, NULL} /* sentinel */ };