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

Sign up to get free protection for your applications and to get access to all the features.
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