mirror of https://github.com/python/cpython
gh-78502: Add a trackfd parameter to mmap.mmap() (GH-25425)
If *trackfd* is False, the file descriptor specified by *fileno* will not be duplicated. Co-authored-by: Erlend E. Aasland <erlend@python.org> Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
42b90cf0d6
commit
8fd287b18f
|
@ -48,7 +48,7 @@ update the underlying file.
|
|||
|
||||
To map anonymous memory, -1 should be passed as the fileno along with the length.
|
||||
|
||||
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])
|
||||
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
|
||||
|
||||
**(Windows version)** Maps *length* bytes from the file specified by the
|
||||
file handle *fileno*, and creates a mmap object. If *length* is larger
|
||||
|
@ -71,7 +71,8 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
|
|||
|
||||
.. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap
|
||||
|
||||
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset])
|
||||
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \
|
||||
access=ACCESS_DEFAULT, offset=0, *, trackfd=True)
|
||||
:noindex:
|
||||
|
||||
**(Unix version)** Maps *length* bytes from the file specified by the file
|
||||
|
@ -102,10 +103,20 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
|
|||
defaults to 0. *offset* must be a multiple of :const:`ALLOCATIONGRANULARITY`
|
||||
which is equal to :const:`PAGESIZE` on Unix systems.
|
||||
|
||||
If *trackfd* is ``False``, the file descriptor specified by *fileno* will
|
||||
not be duplicated, and the resulting :class:`!mmap` object will not
|
||||
be associated with the map's underlying file.
|
||||
This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
|
||||
methods will fail.
|
||||
This mode is useful to limit the number of open file descriptors.
|
||||
|
||||
To ensure validity of the created memory mapping the file specified
|
||||
by the descriptor *fileno* is internally automatically synchronized
|
||||
with the physical backing store on macOS.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
The *trackfd* parameter was added.
|
||||
|
||||
This example shows a simple way of using :class:`~mmap.mmap`::
|
||||
|
||||
import mmap
|
||||
|
@ -254,9 +265,12 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
|
|||
|
||||
.. method:: resize(newsize)
|
||||
|
||||
Resizes the map and the underlying file, if any. If the mmap was created
|
||||
with :const:`ACCESS_READ` or :const:`ACCESS_COPY`, resizing the map will
|
||||
raise a :exc:`TypeError` exception.
|
||||
Resizes the map and the underlying file, if any.
|
||||
|
||||
Resizing a map created with *access* of :const:`ACCESS_READ` or
|
||||
:const:`ACCESS_COPY`, will raise a :exc:`TypeError` exception.
|
||||
Resizing a map created with with *trackfd* set to ``False``,
|
||||
will raise a :exc:`ValueError` exception.
|
||||
|
||||
**On Windows**: Resizing the map will raise an :exc:`OSError` if there are other
|
||||
maps against the same named file. Resizing an anonymous map (ie against the
|
||||
|
|
|
@ -254,6 +254,9 @@ mmap
|
|||
that can be used where it requires a file-like object with seekable and
|
||||
the :meth:`~mmap.mmap.seek` method return the new absolute position.
|
||||
(Contributed by Donghee Na and Sylvie Liberman in :gh:`111835`.)
|
||||
* :class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is ``False``,
|
||||
the file descriptor specified by *fileno* will not be duplicated.
|
||||
(Contributed by Zackery Spytz and Petr Viktorin in :gh:`78502`.)
|
||||
|
||||
opcode
|
||||
------
|
||||
|
|
|
@ -4,6 +4,7 @@ from test.support import (
|
|||
from test.support.import_helper import import_module
|
||||
from test.support.os_helper import TESTFN, unlink
|
||||
import unittest
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import itertools
|
||||
|
@ -266,6 +267,62 @@ class MmapTests(unittest.TestCase):
|
|||
self.assertRaises(TypeError, m.write_byte, 0)
|
||||
m.close()
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
|
||||
def test_trackfd_parameter(self):
|
||||
size = 64
|
||||
with open(TESTFN, "wb") as f:
|
||||
f.write(b"a"*size)
|
||||
for close_original_fd in True, False:
|
||||
with self.subTest(close_original_fd=close_original_fd):
|
||||
with open(TESTFN, "r+b") as f:
|
||||
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
|
||||
if close_original_fd:
|
||||
f.close()
|
||||
self.assertEqual(len(m), size)
|
||||
with self.assertRaises(OSError) as err_cm:
|
||||
m.size()
|
||||
self.assertEqual(err_cm.exception.errno, errno.EBADF)
|
||||
with self.assertRaises(ValueError):
|
||||
m.resize(size * 2)
|
||||
with self.assertRaises(ValueError):
|
||||
m.resize(size // 2)
|
||||
self.assertEqual(m.closed, False)
|
||||
|
||||
# Smoke-test other API
|
||||
m.write_byte(ord('X'))
|
||||
m[2] = ord('Y')
|
||||
m.flush()
|
||||
with open(TESTFN, "rb") as f:
|
||||
self.assertEqual(f.read(4), b'XaYa')
|
||||
self.assertEqual(m.tell(), 1)
|
||||
m.seek(0)
|
||||
self.assertEqual(m.tell(), 0)
|
||||
self.assertEqual(m.read_byte(), ord('X'))
|
||||
|
||||
self.assertEqual(m.closed, True)
|
||||
self.assertEqual(os.stat(TESTFN).st_size, size)
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
|
||||
def test_trackfd_neg1(self):
|
||||
size = 64
|
||||
with mmap.mmap(-1, size, trackfd=False) as m:
|
||||
with self.assertRaises(OSError):
|
||||
m.size()
|
||||
with self.assertRaises(ValueError):
|
||||
m.resize(size // 2)
|
||||
self.assertEqual(len(m), size)
|
||||
m[0] = ord('a')
|
||||
assert m[0] == ord('a')
|
||||
|
||||
@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
|
||||
def test_no_trackfd_parameter_on_windows(self):
|
||||
# 'trackffd' is an invalid keyword argument for this function
|
||||
size = 64
|
||||
with self.assertRaises(TypeError):
|
||||
mmap.mmap(-1, size, trackfd=True)
|
||||
with self.assertRaises(TypeError):
|
||||
mmap.mmap(-1, size, trackfd=False)
|
||||
|
||||
def test_bad_file_desc(self):
|
||||
# Try opening a bad file descriptor...
|
||||
self.assertRaises(OSError, mmap.mmap, -2, 4096)
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
:class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is
|
||||
``False``, the file descriptor specified by *fileno* will not be duplicated.
|
|
@ -117,6 +117,7 @@ typedef struct {
|
|||
|
||||
#ifdef UNIX
|
||||
int fd;
|
||||
_Bool trackfd;
|
||||
#endif
|
||||
|
||||
PyObject *weakreflist;
|
||||
|
@ -393,6 +394,13 @@ is_resizeable(mmap_object *self)
|
|||
"mmap can't resize with extant buffers exported.");
|
||||
return 0;
|
||||
}
|
||||
#ifdef UNIX
|
||||
if (!self->trackfd) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"mmap can't resize with trackfd=False.");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
|
||||
return 1;
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
|
@ -1154,7 +1162,7 @@ is 0, the maximum length of the map is the current size of the file,\n\
|
|||
except that if the file is empty Windows raises an exception (you cannot\n\
|
||||
create an empty mapping on Windows).\n\
|
||||
\n\
|
||||
Unix: mmap(fileno, length[, flags[, prot[, access[, offset]]]])\n\
|
||||
Unix: mmap(fileno, length[, flags[, prot[, access[, offset[, trackfd]]]]])\n\
|
||||
\n\
|
||||
Maps length bytes from the file specified by the file descriptor fileno,\n\
|
||||
and returns a mmap object. If length is 0, the maximum length of the map\n\
|
||||
|
@ -1221,15 +1229,17 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
|
|||
off_t offset = 0;
|
||||
int fd, flags = MAP_SHARED, prot = PROT_WRITE | PROT_READ;
|
||||
int devzero = -1;
|
||||
int access = (int)ACCESS_DEFAULT;
|
||||
int access = (int)ACCESS_DEFAULT, trackfd = 1;
|
||||
static char *keywords[] = {"fileno", "length",
|
||||
"flags", "prot",
|
||||
"access", "offset", NULL};
|
||||
"access", "offset", "trackfd", NULL};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|iii" _Py_PARSE_OFF_T, keywords,
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwdict,
|
||||
"in|iii" _Py_PARSE_OFF_T "$p", keywords,
|
||||
&fd, &map_size, &flags, &prot,
|
||||
&access, &offset))
|
||||
&access, &offset, &trackfd)) {
|
||||
return NULL;
|
||||
}
|
||||
if (map_size < 0) {
|
||||
PyErr_SetString(PyExc_OverflowError,
|
||||
"memory mapped length must be positive");
|
||||
|
@ -1325,6 +1335,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
|
|||
m_obj->weakreflist = NULL;
|
||||
m_obj->exports = 0;
|
||||
m_obj->offset = offset;
|
||||
m_obj->trackfd = trackfd;
|
||||
if (fd == -1) {
|
||||
m_obj->fd = -1;
|
||||
/* Assume the caller wants to map anonymous memory.
|
||||
|
@ -1350,13 +1361,16 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
|
|||
}
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
else if (trackfd) {
|
||||
m_obj->fd = _Py_dup(fd);
|
||||
if (m_obj->fd == -1) {
|
||||
Py_DECREF(m_obj);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
m_obj->fd = -1;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);
|
||||
|
|
Loading…
Reference in New Issue