omlish 0.0.0.dev224__py3-none-any.whl → 0.0.0.dev225__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/asyncs/asyncio/subprocesses.py +15 -15
  3. omlish/asyncs/asyncs.py +0 -1
  4. omlish/bootstrap/sys.py +2 -2
  5. omlish/dataclasses/impl/metaclass.py +5 -0
  6. omlish/http/coro/server.py +5 -54
  7. omlish/http/coro/simple.py +1 -1
  8. omlish/http/coro/sockets.py +59 -0
  9. omlish/lang/__init__.py +1 -0
  10. omlish/lang/imports.py +22 -0
  11. omlish/libc.py +10 -0
  12. omlish/multiprocessing/__init__.py +0 -7
  13. omlish/os/pidfiles/__init__.py +0 -0
  14. omlish/os/pidfiles/manager.py +97 -0
  15. omlish/os/pidfiles/pidfile.py +142 -0
  16. omlish/secrets/crypto.py +1 -2
  17. omlish/secrets/openssl.py +1 -1
  18. omlish/secrets/tempssl.py +4 -7
  19. omlish/sockets/handlers.py +4 -0
  20. omlish/sockets/server/handlers.py +22 -0
  21. omlish/subprocesses/__init__.py +0 -0
  22. omlish/subprocesses/async_.py +96 -0
  23. omlish/subprocesses/base.py +215 -0
  24. omlish/subprocesses/run.py +98 -0
  25. omlish/subprocesses/sync.py +147 -0
  26. omlish/subprocesses/utils.py +22 -0
  27. omlish/subprocesses/wrap.py +23 -0
  28. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/METADATA +1 -1
  29. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/RECORD +34 -25
  30. omlish/os/pidfile.py +0 -69
  31. omlish/subprocesses.py +0 -510
  32. /omlish/{multiprocessing → os}/death.py +0 -0
  33. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/LICENSE +0 -0
  34. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/WHEEL +0 -0
  35. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/entry_points.txt +0 -0
  36. {omlish-0.0.0.dev224.dist-info → omlish-0.0.0.dev225.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev224'
2
- __revision__ = '0980d15b9ba0f06f68cf7bdb68fbc8626696075f'
1
+ __version__ = '0.0.0.dev225'
2
+ __revision__ = '99eb1740a6c8647c20c80278b6d0bedff0f7c103'
3
3
 
4
4
 
5
5
  #
@@ -10,9 +10,9 @@ import sys
10
10
  import typing as ta
11
11
 
12
12
  from ...lite.check import check
13
- from ...subprocesses import AbstractAsyncSubprocesses
14
- from ...subprocesses import SubprocessRun
15
- from ...subprocesses import SubprocessRunOutput
13
+ from ...subprocesses.async_ import AbstractAsyncSubprocesses
14
+ from ...subprocesses.run import SubprocessRun
15
+ from ...subprocesses.run import SubprocessRunOutput
16
16
  from .timeouts import asyncio_maybe_timeout
17
17
 
18
18
 
@@ -157,19 +157,19 @@ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
157
157
  timeout: ta.Optional[float] = None,
158
158
  **kwargs: ta.Any,
159
159
  ) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
160
- fac: ta.Any
161
- if shell:
162
- fac = functools.partial(
163
- asyncio.create_subprocess_shell,
164
- check.single(cmd),
165
- )
166
- else:
167
- fac = functools.partial(
168
- asyncio.create_subprocess_exec,
169
- *cmd,
170
- )
171
-
172
160
  with self.prepare_and_wrap( *cmd, shell=shell, **kwargs) as (cmd, kwargs): # noqa
161
+ fac: ta.Any
162
+ if shell:
163
+ fac = functools.partial(
164
+ asyncio.create_subprocess_shell,
165
+ check.single(cmd),
166
+ )
167
+ else:
168
+ fac = functools.partial(
169
+ asyncio.create_subprocess_exec,
170
+ *cmd,
171
+ )
172
+
173
173
  proc: asyncio.subprocess.Process = await fac(**kwargs)
174
174
  try:
175
175
  yield proc
omlish/asyncs/asyncs.py CHANGED
@@ -55,7 +55,6 @@ async def async_list(fn: ta.Callable[..., ta.AsyncIterator[T]], *args, **kwargs)
55
55
 
56
56
 
57
57
  class SyncableIterable(ta.Generic[T]):
58
-
59
58
  def __init__(self, obj) -> None:
60
59
  super().__init__()
61
60
  self._obj = obj
omlish/bootstrap/sys.py CHANGED
@@ -23,13 +23,13 @@ if ta.TYPE_CHECKING:
23
23
  from .. import libc
24
24
  from ..formats import dotenv
25
25
  from ..logs import all as logs
26
- from ..os import pidfile
26
+ from ..os.pidfiles import pidfile
27
27
 
28
28
  else:
29
29
  libc = lang.proxy_import('..libc', __package__)
30
30
  logs = lang.proxy_import('..logs', __package__)
31
31
  dotenv = lang.proxy_import('..formats.dotenv', __package__)
32
- pidfile = lang.proxy_import('..os.pidfile', __package__)
32
+ pidfile = lang.proxy_import('..os.pidfiles.pidfile', __package__)
33
33
 
34
34
 
35
35
  ##
@@ -188,8 +188,13 @@ class Data(
188
188
  metaclass=DataMeta,
189
189
  ):
190
190
  def __init__(self, *args, **kwargs):
191
+ # Typechecking barrier
191
192
  super().__init__(*args, **kwargs)
192
193
 
194
+ def __init_subclass__(cls, **kwargs):
195
+ # Typechecking barrier
196
+ super().__init_subclass__(**kwargs)
197
+
193
198
  def __post_init__(self, *args, **kwargs) -> None:
194
199
  try:
195
200
  spi = super().__post_init__ # type: ignore # noqa
@@ -65,8 +65,6 @@ import typing as ta
65
65
 
66
66
  from ...lite.check import check
67
67
  from ...sockets.addresses import SocketAddress
68
- from ...sockets.handlers import SocketHandler_
69
- from ...sockets.io import SocketIoPair
70
68
  from ..handlers import HttpHandler
71
69
  from ..handlers import HttpHandlerRequest
72
70
  from ..handlers import HttpHandlerResponseData
@@ -423,6 +421,9 @@ class CoroHttpServer:
423
421
  def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
424
422
  return self._coro_run_handler(self._coro_handle_one())
425
423
 
424
+ class Close(Exception): # noqa
425
+ pass
426
+
426
427
  def _coro_run_handler(
427
428
  self,
428
429
  gen: ta.Generator[
@@ -462,7 +463,7 @@ class CoroHttpServer:
462
463
 
463
464
  try:
464
465
  o = gen.send(i)
465
- except EOFError:
466
+ except self.Close:
466
467
  return
467
468
  except StopIteration:
468
469
  break
@@ -491,7 +492,7 @@ class CoroHttpServer:
491
492
  break
492
493
 
493
494
  if isinstance(parsed, EmptyParsedHttpResult):
494
- raise EOFError # noqa
495
+ raise self.Close
495
496
 
496
497
  if isinstance(parsed, ParseHttpRequestError):
497
498
  err = self._build_error(
@@ -581,53 +582,3 @@ class CoroHttpServer:
581
582
  handler_response.close()
582
583
 
583
584
  raise
584
-
585
-
586
- ##
587
-
588
-
589
- class CoroHttpServerSocketHandler(SocketHandler_):
590
- def __init__(
591
- self,
592
- server_factory: CoroHttpServerFactory,
593
- *,
594
- log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
595
- ) -> None:
596
- super().__init__()
597
-
598
- self._server_factory = server_factory
599
- self._log_handler = log_handler
600
-
601
- def __call__(self, client_address: SocketAddress, fp: SocketIoPair) -> None:
602
- server = self._server_factory(client_address)
603
-
604
- gen = server.coro_handle()
605
-
606
- o = next(gen)
607
- while True:
608
- if isinstance(o, CoroHttpServer.AnyLogIo):
609
- i = None
610
- if self._log_handler is not None:
611
- self._log_handler(server, o)
612
-
613
- elif isinstance(o, CoroHttpServer.ReadIo):
614
- i = fp.r.read(o.sz)
615
-
616
- elif isinstance(o, CoroHttpServer.ReadLineIo):
617
- i = fp.r.readline(o.sz)
618
-
619
- elif isinstance(o, CoroHttpServer.WriteIo):
620
- i = None
621
- fp.w.write(o.data)
622
- fp.w.flush()
623
-
624
- else:
625
- raise TypeError(o)
626
-
627
- try:
628
- if i is not None:
629
- o = gen.send(i)
630
- else:
631
- o = next(gen)
632
- except StopIteration:
633
- break
@@ -21,7 +21,7 @@ from ..parsing import HttpRequestParser
21
21
  from ..versions import HttpProtocolVersion
22
22
  from ..versions import HttpProtocolVersions
23
23
  from .server import CoroHttpServer
24
- from .server import CoroHttpServerSocketHandler
24
+ from .sockets import CoroHttpServerSocketHandler
25
25
 
26
26
 
27
27
  if ta.TYPE_CHECKING:
@@ -0,0 +1,59 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import typing as ta
4
+
5
+ from ...sockets.addresses import SocketAddress
6
+ from ...sockets.handlers import SocketHandler_
7
+ from ...sockets.io import SocketIoPair
8
+ from .server import CoroHttpServer
9
+ from .server import CoroHttpServerFactory
10
+
11
+
12
+ ##
13
+
14
+
15
+ class CoroHttpServerSocketHandler(SocketHandler_):
16
+ def __init__(
17
+ self,
18
+ server_factory: CoroHttpServerFactory,
19
+ *,
20
+ log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
21
+ ) -> None:
22
+ super().__init__()
23
+
24
+ self._server_factory = server_factory
25
+ self._log_handler = log_handler
26
+
27
+ def __call__(self, client_address: SocketAddress, fp: SocketIoPair) -> None:
28
+ server = self._server_factory(client_address)
29
+
30
+ gen = server.coro_handle()
31
+
32
+ o = next(gen)
33
+ while True:
34
+ if isinstance(o, CoroHttpServer.AnyLogIo):
35
+ i = None
36
+ if self._log_handler is not None:
37
+ self._log_handler(server, o)
38
+
39
+ elif isinstance(o, CoroHttpServer.ReadIo):
40
+ i = fp.r.read(o.sz)
41
+
42
+ elif isinstance(o, CoroHttpServer.ReadLineIo):
43
+ i = fp.r.readline(o.sz)
44
+
45
+ elif isinstance(o, CoroHttpServer.WriteIo):
46
+ i = None
47
+ fp.w.write(o.data)
48
+ fp.w.flush()
49
+
50
+ else:
51
+ raise TypeError(o)
52
+
53
+ try:
54
+ if i is not None:
55
+ o = gen.send(i)
56
+ else:
57
+ o = next(gen)
58
+ except StopIteration:
59
+ break
omlish/lang/__init__.py CHANGED
@@ -138,6 +138,7 @@ from .generators import ( # noqa
138
138
  from .imports import ( # noqa
139
139
  can_import,
140
140
  import_all,
141
+ import_attr,
141
142
  import_module,
142
143
  import_module_attr,
143
144
  lazy_import,
omlish/lang/imports.py CHANGED
@@ -108,6 +108,28 @@ def import_module_attr(dotted_path: str) -> ta.Any:
108
108
  raise AttributeError(f'Module {module_name!r} has no attr {class_name!r}') from None
109
109
 
110
110
 
111
+ def import_attr(dotted_path: str) -> ta.Any:
112
+ parts = dotted_path.split('.')
113
+ mod: ta.Any = None
114
+ mod_pos = 0
115
+ while mod_pos < len(parts):
116
+ mod_name = '.'.join(parts[:mod_pos + 1])
117
+ try:
118
+ mod = importlib.import_module(mod_name)
119
+ except ImportError:
120
+ break
121
+ mod_pos += 1
122
+ if mod is None:
123
+ raise ImportError(dotted_path)
124
+ obj = mod
125
+ for att_pos in range(mod_pos, len(parts)):
126
+ obj = getattr(obj, parts[att_pos])
127
+ return obj
128
+
129
+
130
+ ##
131
+
132
+
111
133
  SPECIAL_IMPORTABLE: ta.AbstractSet[str] = frozenset([
112
134
  '__init__.py',
113
135
  '__main__.py',
omlish/libc.py CHANGED
@@ -392,6 +392,14 @@ if LINUX or DARWIN:
392
392
  return CMSG_ALIGN(ct.sizeof(cmsghdr)) + sz
393
393
 
394
394
  def sendfd(sock, fd, data='.'):
395
+ """
396
+ Note: stdlib as of 3.7:
397
+
398
+ https://github.com/python/cpython/blob/84ed9a68bd9a13252b376b21a9167dabae254325/Lib/multiprocessing/reduction.py#L141
399
+
400
+ But still kept around due to other use of cmsg machinery.
401
+ """ # noqa
402
+
395
403
  if not data:
396
404
  raise ValueError(data)
397
405
 
@@ -424,6 +432,8 @@ if LINUX or DARWIN:
424
432
  return libc.sendmsg(sock, msgh, 0)
425
433
 
426
434
  def recvfd(sock, buf_len=4096):
435
+ """See sendfd."""
436
+
427
437
  if buf_len < 1:
428
438
  raise ValueError(buf_len)
429
439
 
@@ -4,13 +4,6 @@ import typing as _ta
4
4
  from .. import lang as _lang
5
5
 
6
6
 
7
- from .death import ( # noqa
8
- BaseDeathpact,
9
- Deathpact,
10
- NopDeathpact,
11
- PipeDeathpact,
12
- )
13
-
14
7
  from .proxies import ( # noqa
15
8
  DummyValueProxy,
16
9
  ValueProxy,
File without changes
@@ -0,0 +1,97 @@
1
+ # ruff: noqa: UP007
2
+ # @omlish-lite
3
+ import contextlib
4
+ import os
5
+ import threading
6
+ import typing as ta
7
+ import weakref
8
+
9
+ from ...lite.check import check
10
+ from .pidfile import Pidfile
11
+
12
+
13
+ ##
14
+
15
+
16
+ class _PidfileManager:
17
+ def __new__(cls, *args, **kwargs): # noqa
18
+ raise TypeError
19
+
20
+ _lock: ta.ClassVar[threading.Lock] = threading.Lock()
21
+ _installed: ta.ClassVar[bool] = False
22
+ _pidfile_threads: ta.ClassVar[ta.MutableMapping[Pidfile, threading.Thread]] = weakref.WeakKeyDictionary()
23
+
24
+ @classmethod
25
+ def _before_fork(cls) -> None:
26
+ cls._lock.acquire()
27
+
28
+ @classmethod
29
+ def _after_fork_in_parent(cls) -> None:
30
+ cls._lock.release()
31
+
32
+ @classmethod
33
+ def _after_fork_in_child(cls) -> None:
34
+ th = threading.current_thread()
35
+ for pf, pf_th in list(cls._pidfile_threads.items()):
36
+ if pf_th is not th:
37
+ pf.close()
38
+ del cls._pidfile_threads[pf]
39
+
40
+ cls._lock.release()
41
+
42
+ #
43
+
44
+ @classmethod
45
+ def _install(cls) -> None:
46
+ check.state(not cls._installed)
47
+
48
+ os.register_at_fork(
49
+ before=cls._before_fork,
50
+ after_in_parent=cls._after_fork_in_parent,
51
+ after_in_child=cls._after_fork_in_child,
52
+ )
53
+
54
+ cls._installed = True
55
+
56
+ @classmethod
57
+ def install(cls) -> bool:
58
+ with cls._lock:
59
+ if cls._installed:
60
+ return False
61
+
62
+ cls._install()
63
+ return True
64
+
65
+ @classmethod
66
+ @contextlib.contextmanager
67
+ def inheritable_pidfile_context(
68
+ cls,
69
+ path: str,
70
+ *,
71
+ inheritable: bool = True,
72
+ **kwargs: ta.Any,
73
+ ) -> ta.Iterator[Pidfile]:
74
+ check.arg(inheritable)
75
+
76
+ cls.install()
77
+
78
+ pf = Pidfile(
79
+ path,
80
+ inheritable=False,
81
+ **kwargs,
82
+ )
83
+
84
+ with cls._lock:
85
+ cls._pidfile_threads[pf] = threading.current_thread()
86
+ try:
87
+ with pf:
88
+ os.set_inheritable(check.not_none(pf.fileno()), True)
89
+ yield pf
90
+
91
+ finally:
92
+ with cls._lock:
93
+ del cls._pidfile_threads[pf]
94
+
95
+
96
+ def open_inheritable_pidfile(path: str, **kwargs: ta.Any) -> ta.ContextManager[Pidfile]:
97
+ return _PidfileManager.inheritable_pidfile_context(path, **kwargs) # noqa
@@ -0,0 +1,142 @@
1
+ # ruff: noqa: UP007
2
+ # @omlish-lite
3
+ """
4
+ TODO:
5
+ - reliable pid retrieval
6
+ - contents are *ignored*, just advisory
7
+ - check double-check:
8
+ - 1) get pid of flock holder
9
+ - 2) get pidfd to that
10
+ - 3) recheck current pid of flock holder == that pid
11
+ - racy as to if it's a different actual process as initial check, just with same pid, but due to 'identity' / semantic
12
+ meaning of the named pidfile the processes are considered equivalent
13
+ """
14
+ import fcntl
15
+ import os
16
+ import signal
17
+ import typing as ta
18
+
19
+
20
+ ##
21
+
22
+
23
+ class Pidfile:
24
+ def __init__(
25
+ self,
26
+ path: str,
27
+ *,
28
+ inheritable: bool = True,
29
+ ) -> None:
30
+ super().__init__()
31
+
32
+ self._path = path
33
+ self._inheritable = inheritable
34
+
35
+ def __repr__(self) -> str:
36
+ return f'{self.__class__.__name__}({self._path!r})'
37
+
38
+ #
39
+
40
+ _f: ta.TextIO
41
+
42
+ def fileno(self) -> ta.Optional[int]:
43
+ if hasattr(self, '_f'):
44
+ return self._f.fileno()
45
+ else:
46
+ return None
47
+
48
+ #
49
+
50
+ def __enter__(self) -> 'Pidfile':
51
+ fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o600)
52
+
53
+ try:
54
+ if self._inheritable:
55
+ os.set_inheritable(fd, True)
56
+
57
+ f = os.fdopen(fd, 'r+')
58
+
59
+ except Exception:
60
+ try:
61
+ os.close(fd)
62
+ except Exception: # noqa
63
+ pass
64
+ raise
65
+
66
+ self._f = f
67
+ return self
68
+
69
+ def __exit__(self, exc_type, exc_val, exc_tb):
70
+ self.close()
71
+
72
+ #
73
+
74
+ def __getstate__(self):
75
+ state = self.__dict__.copy()
76
+
77
+ if '_f' in state:
78
+ if os.get_inheritable(fd := state.pop('_f').fileno()):
79
+ state['__fd'] = fd
80
+
81
+ return state
82
+
83
+ def __setstate__(self, state):
84
+ if '_f' in state:
85
+ raise RuntimeError
86
+
87
+ if '__fd' in state:
88
+ state['_f'] = os.fdopen(state.pop('__fd'), 'r+')
89
+
90
+ self.__dict__.update(state)
91
+
92
+ #
93
+
94
+ def close(self) -> bool:
95
+ if not hasattr(self, '_f'):
96
+ return False
97
+
98
+ self._f.close()
99
+ del self._f
100
+ return True
101
+
102
+ def try_lock(self) -> bool:
103
+ try:
104
+ fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
105
+ return True
106
+
107
+ except OSError:
108
+ return False
109
+
110
+ def ensure_locked(self) -> None:
111
+ if not self.try_lock():
112
+ raise RuntimeError('Could not get lock')
113
+
114
+ #
115
+
116
+ def write(self, pid: ta.Optional[int] = None) -> None:
117
+ self.ensure_locked()
118
+
119
+ if pid is None:
120
+ pid = os.getpid()
121
+
122
+ self._f.seek(0)
123
+ self._f.truncate()
124
+ self._f.write(f'{pid}\n')
125
+ self._f.flush()
126
+
127
+ def clear(self) -> None:
128
+ self.ensure_locked()
129
+
130
+ self._f.seek(0)
131
+ self._f.truncate()
132
+
133
+ def read(self) -> int:
134
+ if self.try_lock():
135
+ raise RuntimeError('Got lock')
136
+
137
+ self._f.seek(0)
138
+ return int(self._f.read()) # FIXME: could be empty or hold old value, race w proc start
139
+
140
+ def kill(self, sig: int = signal.SIGTERM) -> None:
141
+ pid = self.read()
142
+ os.kill(pid, sig) # FIXME: Still racy - pidfd_send_signal?
omlish/secrets/crypto.py CHANGED
@@ -76,7 +76,6 @@ class Crypto(abc.ABC):
76
76
 
77
77
 
78
78
  class FernetCrypto(Crypto):
79
-
80
79
  def generate_key(self) -> bytes:
81
80
  return cry_fernet.Fernet.generate_key()
82
81
 
@@ -98,7 +97,7 @@ class FernetCrypto(Crypto):
98
97
  raise DecryptionError from e
99
98
 
100
99
 
101
- class AesgsmCrypto(Crypto):
100
+ class AesgcmCrypto(Crypto):
102
101
  """https://stackoverflow.com/a/59835994"""
103
102
 
104
103
  def generate_key(self) -> bytes:
omlish/secrets/openssl.py CHANGED
@@ -33,7 +33,7 @@ else:
33
33
  DEFAULT_KEY_SIZE = 64
34
34
 
35
35
 
36
- def generate_key(self, sz: int = DEFAULT_KEY_SIZE) -> bytes:
36
+ def generate_key(sz: int = DEFAULT_KEY_SIZE) -> bytes:
37
37
  # !! https://docs.openssl.org/3.0/man7/passphrase-encoding/
38
38
  # Must not contain null bytes!
39
39
  return secrets.token_hex(sz).encode('ascii')
omlish/secrets/tempssl.py CHANGED
@@ -6,9 +6,9 @@ import tempfile
6
6
  import typing as ta
7
7
 
8
8
  from ..lite.cached import cached_nullary
9
- from ..subprocesses import SubprocessRun
10
- from ..subprocesses import SubprocessRunOutput
11
- from ..subprocesses import subprocesses
9
+ from ..subprocesses.run import SubprocessRun
10
+ from ..subprocesses.run import SubprocessRunnable
11
+ from ..subprocesses.run import SubprocessRunOutput
12
12
  from .ssl import SslCert
13
13
 
14
14
 
@@ -18,7 +18,7 @@ class TempSslCert(ta.NamedTuple):
18
18
 
19
19
 
20
20
  @dc.dataclass(frozen=True)
21
- class TempSslCertGenerator:
21
+ class TempSslCertGenerator(SubprocessRunnable[TempSslCert]):
22
22
  @cached_nullary
23
23
  def temp_dir(self) -> str:
24
24
  return tempfile.mkdtemp()
@@ -64,9 +64,6 @@ class TempSslCertGenerator:
64
64
  temp_dir=self.temp_dir(),
65
65
  )
66
66
 
67
- def run(self) -> TempSslCert:
68
- return self.handle_run_output(subprocesses.run_(self.make_run()))
69
-
70
67
 
71
68
  def generate_temp_localhost_ssl_cert() -> TempSslCert:
72
69
  return TempSslCertGenerator().run()
@@ -17,3 +17,7 @@ class SocketHandler_(abc.ABC): # noqa
17
17
  @abc.abstractmethod
18
18
  def __call__(self, addr: SocketAddress, f: SocketIoPair) -> None:
19
19
  raise NotImplementedError
20
+
21
+
22
+ class SocketHandlerClose(Exception): # noqa
23
+ pass
@@ -3,6 +3,7 @@
3
3
  import abc
4
4
  import concurrent.futures as cf
5
5
  import dataclasses as dc
6
+ import logging
6
7
  import socket
7
8
  import typing as ta
8
9
 
@@ -132,3 +133,24 @@ class ExecutorSocketServerHandler(SocketServerHandler_):
132
133
 
133
134
  def __call__(self, conn: SocketAndAddress) -> None:
134
135
  self.executor.submit(self.handler, conn)
136
+
137
+
138
+ #
139
+
140
+
141
+ @dc.dataclass(frozen=True)
142
+ class ExceptionLoggingSocketServerHandler(SocketServerHandler_):
143
+ handler: SocketServerHandler
144
+ log: logging.Logger
145
+
146
+ ignored: ta.Optional[ta.Container[ta.Type[Exception]]] = None
147
+
148
+ def __call__(self, conn: SocketAndAddress) -> None:
149
+ try:
150
+ return self.handler(conn)
151
+
152
+ except Exception as e: # noqa
153
+ if (ignored := self.ignored) is None or type(e) not in ignored:
154
+ self.log.exception('Error in handler %r for conn %r', self.handler, conn)
155
+
156
+ raise
File without changes