Access to POSIX shared memory from Python

This commit is contained in:
Kovid Goyal 2022-03-10 06:30:03 +05:30
parent aa4d36cc57
commit fd0262413e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 128 additions and 0 deletions

View File

@ -156,6 +156,24 @@ close_tty(PyObject *self UNUSED, PyObject *args) {
#undef TTY_ARGS
static PyObject*
py_shm_open(PyObject UNUSED *self, PyObject *args) {
char *name;
int flags, mode = 0600;
if (!PyArg_ParseTuple(args, "si|i", &name, &flags, &mode)) return NULL;
long fd = safe_shm_open(name, flags, mode);
if (fd < 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
return PyLong_FromLong(fd);
}
static PyObject*
py_shm_unlink(PyObject UNUSED *self, PyObject *args) {
char *name;
if (!PyArg_ParseTuple(args, "s", &name)) return NULL;
if (shm_unlink(name) != 0) return PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, PyTuple_GET_ITEM(args, 0));
Py_RETURN_NONE;
}
static PyObject*
wcwidth_wrap(PyObject UNUSED *self, PyObject *chr) {
return PyLong_FromLong(wcwidth_std(PyLong_AsLong(chr)));
@ -184,6 +202,8 @@ static PyMethodDef module_methods[] = {
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
{"locale_is_valid", (PyCFunction)locale_is_valid, METH_VARARGS, ""},
{"shm_open", (PyCFunction)py_shm_open, METH_VARARGS, ""},
{"shm_unlink", (PyCFunction)py_shm_unlink, METH_VARARGS, ""},
#ifdef __APPLE__
METHODB(user_cache_dir, METH_NOARGS),
METHODB(process_group_map, METH_NOARGS),

View File

@ -1361,3 +1361,11 @@ def set_os_window_title(os_window_id: int, title: str) -> None:
def update_ime_position_for_window(window_id: int, force: bool = False, lost_focus: bool = False) -> bool:
pass
def shm_open(name: str, flags: int, mode: int = 0o600) -> int:
pass
def shm_unlink(name: str) -> None:
pass

100
kitty/shm.py Normal file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
# This is present in the python stdlib after version 3.7 but we need to support
# 3.7 for another year, so sigh.
import mmap
import os
import secrets
from typing import Optional
from kitty.fast_data_types import shm_open, shm_unlink
def make_filename(safe_length: int = 14, prefix: str = '/ky-') -> str:
"Create a random filename for the shared memory object."
# number of random bytes to use for name
nbytes = (safe_length - len(prefix)) // 2
name = prefix + secrets.token_hex(nbytes)
return name
class SharedMemory:
def __init__(self, name: Optional[str] = None, create: bool = False, size: int = 0, readonly: bool = False, mode: int = 0o600):
if not size >= 0:
raise ValueError("'size' must be a positive integer")
if create:
flags = os.O_CREAT | os.O_EXCL
if size <= 0:
raise ValueError("'size' must be > 0")
else:
flags = os.O_RDONLY if readonly else os.O_RDWR
if name is None and not flags & os.O_EXCL:
raise ValueError("'name' can only be None if create=True")
if name is None:
while True:
name = make_filename()
try:
self._fd = shm_open(name, flags, mode)
except FileExistsError:
continue
self._name = name
break
self._name = name
try:
if create and size:
os.ftruncate(self._fd, size)
stats = os.fstat(self._fd)
size = stats.st_size
self._mmap = mmap.mmap(self._fd, size)
except OSError:
self.unlink()
raise
self.size = size
self._buf: Optional[memoryview] = memoryview(self._mmap)
def __del__(self) -> None:
try:
self.close()
except OSError:
pass
@property
def name(self) -> str:
return self._name
@property
def buf(self) -> memoryview:
ans = self._buf
if ans is None:
raise RuntimeError('Cannot access the buffer of a closed shared memory object')
return ans
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._buf is not None:
self._buf.release()
self._buf = None
if self._mmap is not None:
self._mmap.close()
if self._fd >= 0:
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:
shm_unlink(self._name)
self._name = ''