mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Allow usage of SHM from python
This commit is contained in:
parent
b9cd42edaa
commit
09ce82c760
@ -139,6 +139,8 @@ def find_tests(which_tests=None, exclude_tests=None):
|
|||||||
a(find_tests())
|
a(find_tests())
|
||||||
from calibre.utils.html2text import find_tests
|
from calibre.utils.html2text import find_tests
|
||||||
a(find_tests())
|
a(find_tests())
|
||||||
|
from calibre.utils.shm import find_tests
|
||||||
|
a(find_tests())
|
||||||
from calibre.library.comments import find_tests
|
from calibre.library.comments import find_tests
|
||||||
a(find_tests())
|
a(find_tests())
|
||||||
from calibre.ebooks.compression.palmdoc import find_tests
|
from calibre.ebooks.compression.palmdoc import find_tests
|
||||||
|
243
src/calibre/utils/shm.py
Normal file
243
src/calibre/utils/shm.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import mmap
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import stat
|
||||||
|
import struct
|
||||||
|
from typing import Optional, Union
|
||||||
|
from calibre.constants import iswindows, ismacos
|
||||||
|
|
||||||
|
SHM_NAME_MAX = 30 if ismacos else 254
|
||||||
|
if iswindows:
|
||||||
|
import _winapi
|
||||||
|
else:
|
||||||
|
import _posixshmem
|
||||||
|
|
||||||
|
|
||||||
|
def make_filename(prefix: str) -> str:
|
||||||
|
"Create a random filename for the shared memory object."
|
||||||
|
# number of random bytes to use for name. Use a largeish value
|
||||||
|
# to make double unlink safe.
|
||||||
|
if not iswindows and not prefix.startswith('/'):
|
||||||
|
# FreeBSD requires name to start with /
|
||||||
|
prefix = '/' + prefix
|
||||||
|
plen = len(prefix.encode('utf-8'))
|
||||||
|
safe_length = min(plen + 64, SHM_NAME_MAX)
|
||||||
|
if safe_length - plen < 2:
|
||||||
|
raise OSError(errno.ENAMETOOLONG, f'SHM filename prefix {prefix} is too long')
|
||||||
|
nbytes = (safe_length - plen) // 2
|
||||||
|
name = prefix + secrets.token_hex(nbytes)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class SharedMemory:
|
||||||
|
'''
|
||||||
|
Create or access randomly named shared memory. To create call with empty name and specific size.
|
||||||
|
To access call with name only.
|
||||||
|
|
||||||
|
WARNING: The actual size of the shared memory may be larger than the requested size.
|
||||||
|
'''
|
||||||
|
_fd: int = -1
|
||||||
|
_name: str = ''
|
||||||
|
_mmap: Optional[mmap.mmap] = None
|
||||||
|
_size: int = 0
|
||||||
|
size_fmt = '!I'
|
||||||
|
num_bytes_for_size = struct.calcsize(size_fmt)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str = '', size: int = 0, readonly: bool = False,
|
||||||
|
mode: int = stat.S_IREAD | stat.S_IWRITE,
|
||||||
|
prefix: str = 'calibre-', unlink_on_exit: bool = False
|
||||||
|
):
|
||||||
|
self.unlink_on_exit = unlink_on_exit
|
||||||
|
if size < 0:
|
||||||
|
raise TypeError("'size' must be a non-negative integer")
|
||||||
|
if size and name:
|
||||||
|
raise TypeError('Cannot specify both name and size')
|
||||||
|
if not name:
|
||||||
|
flags = os.O_CREAT | os.O_EXCL
|
||||||
|
if not size:
|
||||||
|
raise TypeError("'size' must be > 0")
|
||||||
|
else:
|
||||||
|
flags = 0
|
||||||
|
flags |= os.O_RDONLY if readonly else os.O_RDWR
|
||||||
|
access = mmap.ACCESS_READ if readonly else mmap.ACCESS_WRITE
|
||||||
|
|
||||||
|
create = not name
|
||||||
|
tries = 30
|
||||||
|
while not name and tries > 0:
|
||||||
|
tries -= 1
|
||||||
|
q = make_filename(prefix)
|
||||||
|
if iswindows:
|
||||||
|
h_map = _winapi.CreateFileMapping(
|
||||||
|
_winapi.INVALID_HANDLE_VALUE,
|
||||||
|
_winapi.NULL,
|
||||||
|
_winapi.PAGE_READONLY if readonly else _winapi.PAGE_READWRITE,
|
||||||
|
(size >> 32) & 0xFFFFFFFF,
|
||||||
|
size & 0xFFFFFFFF,
|
||||||
|
q
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
last_error_code = _winapi.GetLastError()
|
||||||
|
if last_error_code == _winapi.ERROR_ALREADY_EXISTS:
|
||||||
|
continue
|
||||||
|
self._mmap = mmap.mmap(-1, size, tagname=q, access=access)
|
||||||
|
name = q
|
||||||
|
finally:
|
||||||
|
_winapi.CloseHandle(h_map)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._fd = _posixshmem.shm_open(q, flags, mode=mode)
|
||||||
|
name = q
|
||||||
|
except FileExistsError:
|
||||||
|
continue
|
||||||
|
if tries <= 0:
|
||||||
|
raise OSError(f'Failed to create a uniquely named SHM file, try shortening the prefix from: {prefix}')
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
if not create and iswindows:
|
||||||
|
h_map = _winapi.OpenFileMapping(
|
||||||
|
_winapi.FILE_MAP_READ,
|
||||||
|
False,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
p_buf = _winapi.MapViewOfFile(
|
||||||
|
h_map,
|
||||||
|
_winapi.FILE_MAP_READ,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_winapi.CloseHandle(h_map)
|
||||||
|
size = _winapi.VirtualQuerySize(p_buf)
|
||||||
|
self._mmap = mmap.mmap(-1, size, tagname=name)
|
||||||
|
if not iswindows:
|
||||||
|
if not create:
|
||||||
|
self._fd = _posixshmem.shm_open(name, flags, mode)
|
||||||
|
try:
|
||||||
|
if flags & os.O_CREAT and size:
|
||||||
|
os.ftruncate(self._fd, size)
|
||||||
|
self.stats = os.fstat(self._fd)
|
||||||
|
size = self.stats.st_size
|
||||||
|
self._mmap = mmap.mmap(self._fd, size, access=access)
|
||||||
|
except OSError:
|
||||||
|
self.unlink()
|
||||||
|
raise
|
||||||
|
|
||||||
|
self._size = size
|
||||||
|
|
||||||
|
def read(self, sz: int = 0) -> bytes:
|
||||||
|
if sz <= 0:
|
||||||
|
sz = self.size
|
||||||
|
return self.mmap.read(sz)
|
||||||
|
|
||||||
|
def write(self, data: bytes) -> None:
|
||||||
|
self.mmap.write(data)
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.mmap.tell()
|
||||||
|
|
||||||
|
def seek(self, pos: int, whence: int = os.SEEK_SET) -> None:
|
||||||
|
self.mmap.seek(pos, whence)
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
self.mmap.flush()
|
||||||
|
|
||||||
|
def write_data_with_size(self, data: Union[str, bytes]) -> None:
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
sz = struct.pack(self.size_fmt, len(data))
|
||||||
|
self.write(sz)
|
||||||
|
self.write(data)
|
||||||
|
|
||||||
|
def read_data_with_size(self) -> bytes:
|
||||||
|
sz = struct.unpack(self.size_fmt, self.read(self.num_bytes_for_size))[0]
|
||||||
|
return self.read(sz)
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
try:
|
||||||
|
self.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self) -> 'SharedMemory':
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *a: object) -> None:
|
||||||
|
self.close()
|
||||||
|
if self.unlink_on_exit:
|
||||||
|
self.unlink()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
return self._size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mmap(self) -> mmap.mmap:
|
||||||
|
ans = self._mmap
|
||||||
|
if ans is None:
|
||||||
|
raise RuntimeError('Cannot access the mmap of a closed shared memory object')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def fileno(self) -> int:
|
||||||
|
return self._fd
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'{self.__class__.__name__}({self.name!r}, size={self.size})'
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Closes access to the shared memory from this instance but does
|
||||||
|
not destroy the shared memory block."""
|
||||||
|
if self._mmap is not None:
|
||||||
|
self._mmap.close()
|
||||||
|
self._mmap = None
|
||||||
|
if self._fd > -1:
|
||||||
|
os.close(self._fd)
|
||||||
|
self._fd = -1
|
||||||
|
|
||||||
|
def unlink(self) -> None:
|
||||||
|
"""Requests that the underlying shared memory block be destroyed.
|
||||||
|
|
||||||
|
In order to ensure proper cleanup of resources, unlink should be
|
||||||
|
called once (and only once) across all processes which have access
|
||||||
|
to the shared memory block."""
|
||||||
|
if self._name and not iswindows:
|
||||||
|
try:
|
||||||
|
_posixshmem.shm_unlink(self._name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
self._name = ''
|
||||||
|
|
||||||
|
|
||||||
|
def find_tests():
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSHM(unittest.TestCase):
|
||||||
|
ae = unittest.TestCase.assertEqual
|
||||||
|
|
||||||
|
def test_shm(self):
|
||||||
|
with SharedMemory(size=64, unlink_on_exit=True) as shm:
|
||||||
|
q = b'test'
|
||||||
|
shm.write_data_with_size(q)
|
||||||
|
self.ae(shm.tell(), shm.num_bytes_for_size + len(q))
|
||||||
|
shm.flush()
|
||||||
|
with SharedMemory(shm.name, readonly=True) as s2:
|
||||||
|
self.ae(s2.read_data_with_size(), q)
|
||||||
|
shm.write(b'ABCD')
|
||||||
|
shm.flush()
|
||||||
|
self.ae(s2.read(4), b'ABCD')
|
||||||
|
self.assertTrue(shm.name)
|
||||||
|
self.assertFalse(shm.name)
|
||||||
|
|
||||||
|
return unittest.defaultTestLoader.loadTestsFromTestCase(TestSHM)
|
Loading…
x
Reference in New Issue
Block a user