omlish 0.0.0.dev216__py3-none-any.whl → 0.0.0.dev218__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.
- omlish/__about__.py +2 -2
- omlish/asyncs/asyncio/all.py +4 -3
- omlish/dataclasses/__init__.py +6 -2
- omlish/dataclasses/utils.py +0 -12
- omlish/docker/oci/__init__.py +0 -0
- omlish/docker/oci/data.py +71 -0
- omlish/docker/oci/media.py +124 -0
- omlish/docker/portrelay.py +49 -0
- omlish/formats/json5/Json5.g4 +0 -3
- omlish/http/coro/server.py +45 -25
- omlish/http/handlers.py +11 -1
- omlish/iterators/tools.py +1 -0
- omlish/lang/imports.py +16 -8
- omlish/lite/dataclasses.py +3 -1
- omlish/logs/all.py +13 -0
- omlish/logs/callers.py +45 -0
- omlish/logs/protocol.py +176 -0
- omlish/marshal/dataclasses.py +26 -0
- omlish/sockets/addresses.py +13 -4
- omlish/sockets/bind.py +332 -0
- omlish/sockets/handlers.py +2 -20
- omlish/sockets/io.py +69 -0
- omlish/sockets/server/__init__.py +0 -0
- omlish/sockets/server/handlers.py +99 -0
- omlish/sockets/server/server.py +144 -0
- omlish/sockets/server/threading.py +123 -0
- omlish/subprocesses.py +65 -3
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/RECORD +33 -22
- omlish/sockets/server.py +0 -66
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev216.dist-info → omlish-0.0.0.dev218.dist-info}/top_level.txt +0 -0
omlish/logs/protocol.py
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import abc
|
4
|
+
import logging
|
5
|
+
import sys
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from .callers import LoggingCaller
|
9
|
+
|
10
|
+
|
11
|
+
LogLevel = int # ta.TypeAlias
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
|
16
|
+
|
17
|
+
class Logging(ta.Protocol):
|
18
|
+
def isEnabledFor(self, level: LogLevel) -> bool: # noqa
|
19
|
+
...
|
20
|
+
|
21
|
+
def getEffectiveLevel(self) -> LogLevel: # noqa
|
22
|
+
...
|
23
|
+
|
24
|
+
#
|
25
|
+
|
26
|
+
def debug(self, msg: str, *args, **kwargs) -> None:
|
27
|
+
...
|
28
|
+
|
29
|
+
def info(self, msg: str, *args, **kwargs) -> None:
|
30
|
+
...
|
31
|
+
|
32
|
+
def warning(self, msg: str, *args, **kwargs) -> None:
|
33
|
+
...
|
34
|
+
|
35
|
+
def error(self, msg: str, *args, **kwargs) -> None:
|
36
|
+
...
|
37
|
+
|
38
|
+
def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
|
39
|
+
...
|
40
|
+
|
41
|
+
def critical(self, msg: str, *args, **kwargs) -> None:
|
42
|
+
...
|
43
|
+
|
44
|
+
def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
|
45
|
+
...
|
46
|
+
|
47
|
+
|
48
|
+
##
|
49
|
+
|
50
|
+
|
51
|
+
class AbstractLogging(abc.ABC):
|
52
|
+
@ta.final
|
53
|
+
def isEnabledFor(self, level: LogLevel) -> bool: # noqa
|
54
|
+
return self.is_enabled_for(level)
|
55
|
+
|
56
|
+
def is_enabled_for(self, level: LogLevel) -> bool: # noqa
|
57
|
+
return level >= self.getEffectiveLevel()
|
58
|
+
|
59
|
+
@ta.final
|
60
|
+
def getEffectiveLevel(self) -> LogLevel: # noqa
|
61
|
+
return self.get_effective_level()
|
62
|
+
|
63
|
+
@abc.abstractmethod
|
64
|
+
def get_effective_level(self) -> LogLevel: # noqa
|
65
|
+
raise NotImplementedError
|
66
|
+
|
67
|
+
#
|
68
|
+
|
69
|
+
def debug(self, msg: str, *args, **kwargs) -> None:
|
70
|
+
if self.is_enabled_for(logging.DEBUG):
|
71
|
+
self.log(logging.DEBUG, msg, args, **kwargs)
|
72
|
+
|
73
|
+
def info(self, msg: str, *args, **kwargs) -> None:
|
74
|
+
if self.is_enabled_for(logging.INFO):
|
75
|
+
self.log(logging.INFO, msg, args, **kwargs)
|
76
|
+
|
77
|
+
def warning(self, msg: str, *args, **kwargs) -> None:
|
78
|
+
if self.is_enabled_for(logging.WARNING):
|
79
|
+
self.log(logging.WARNING, msg, args, **kwargs)
|
80
|
+
|
81
|
+
def error(self, msg: str, *args, **kwargs) -> None:
|
82
|
+
if self.is_enabled_for(logging.ERROR):
|
83
|
+
self.log(logging.ERROR, msg, args, **kwargs)
|
84
|
+
|
85
|
+
def exception(self, msg: str, *args, exc_info=True, **kwargs) -> None:
|
86
|
+
self.error(msg, *args, exc_info=exc_info, **kwargs)
|
87
|
+
|
88
|
+
def critical(self, msg: str, *args, **kwargs) -> None:
|
89
|
+
if self.is_enabled_for(logging.CRITICAL):
|
90
|
+
self.log(logging.CRITICAL, msg, args, **kwargs)
|
91
|
+
|
92
|
+
def log(self, level: LogLevel, msg: str, *args, **kwargs) -> None:
|
93
|
+
if not isinstance(level, int):
|
94
|
+
raise TypeError('Level must be an integer.')
|
95
|
+
if self.is_enabled_for(level):
|
96
|
+
self._log(level, msg, args, **kwargs)
|
97
|
+
|
98
|
+
@abc.abstractmethod
|
99
|
+
def _log(
|
100
|
+
self,
|
101
|
+
level: int,
|
102
|
+
msg: str,
|
103
|
+
args: ta.Any,
|
104
|
+
*,
|
105
|
+
exc_info: ta.Any = None,
|
106
|
+
extra: ta.Any = None,
|
107
|
+
stack_info: bool = False,
|
108
|
+
) -> None:
|
109
|
+
raise NotImplementedError
|
110
|
+
|
111
|
+
|
112
|
+
##
|
113
|
+
|
114
|
+
|
115
|
+
class NopLogging(AbstractLogging):
|
116
|
+
def get_effective_level(self) -> LogLevel:
|
117
|
+
return logging.CRITICAL + 1
|
118
|
+
|
119
|
+
def _log(self, *args: ta.Any, **kwargs: ta.Any) -> None:
|
120
|
+
pass
|
121
|
+
|
122
|
+
|
123
|
+
##
|
124
|
+
|
125
|
+
|
126
|
+
class StdlibLogging(AbstractLogging):
|
127
|
+
def __init__(self, underlying: logging.Logger) -> None:
|
128
|
+
super().__init__()
|
129
|
+
|
130
|
+
if not isinstance(underlying, logging.Logger):
|
131
|
+
raise TypeError(underlying)
|
132
|
+
|
133
|
+
self._underlying = underlying
|
134
|
+
|
135
|
+
#
|
136
|
+
|
137
|
+
def is_enabled_for(self, level: int) -> bool: # noqa
|
138
|
+
return self._underlying.isEnabledFor(level)
|
139
|
+
|
140
|
+
def get_effective_level(self) -> int: # noqa
|
141
|
+
return self._underlying.getEffectiveLevel()
|
142
|
+
|
143
|
+
#
|
144
|
+
|
145
|
+
def _log(
|
146
|
+
self,
|
147
|
+
level: int,
|
148
|
+
msg: str,
|
149
|
+
args: ta.Any,
|
150
|
+
*,
|
151
|
+
exc_info: ta.Any = None,
|
152
|
+
extra: ta.Any = None,
|
153
|
+
stack_info: bool = False,
|
154
|
+
) -> None:
|
155
|
+
caller = LoggingCaller.find(stack_info)
|
156
|
+
|
157
|
+
if exc_info:
|
158
|
+
if isinstance(exc_info, BaseException):
|
159
|
+
exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
|
160
|
+
elif not isinstance(exc_info, tuple):
|
161
|
+
exc_info = sys.exc_info()
|
162
|
+
|
163
|
+
record = self._underlying.makeRecord(
|
164
|
+
name=self._underlying.name,
|
165
|
+
level=level,
|
166
|
+
fn=caller.filename,
|
167
|
+
lno=caller.lineno,
|
168
|
+
msg=msg,
|
169
|
+
args=args,
|
170
|
+
exc_info=exc_info,
|
171
|
+
func=caller.func,
|
172
|
+
extra=extra,
|
173
|
+
sinfo=caller.sinfo,
|
174
|
+
)
|
175
|
+
|
176
|
+
self._underlying.handle(record)
|
omlish/marshal/dataclasses.py
CHANGED
@@ -9,6 +9,7 @@ from .. import collections as col
|
|
9
9
|
from .. import dataclasses as dc
|
10
10
|
from .. import lang
|
11
11
|
from .. import reflect as rfl
|
12
|
+
from ..lite import marshal as lm
|
12
13
|
from .base import MarshalContext
|
13
14
|
from .base import Marshaler
|
14
15
|
from .base import MarshalerFactory
|
@@ -96,6 +97,31 @@ def get_field_infos(
|
|
96
97
|
unmarshal_names=col.unique([fmd.name, *(fmd.alts or ())]),
|
97
98
|
)
|
98
99
|
|
100
|
+
else:
|
101
|
+
try:
|
102
|
+
lfk = field.metadata[lm.OBJ_MARSHALER_FIELD_KEY]
|
103
|
+
except KeyError:
|
104
|
+
pass
|
105
|
+
else:
|
106
|
+
if lfk is not None:
|
107
|
+
check.non_empty_str(lfk)
|
108
|
+
has_set_name = True
|
109
|
+
fi_kw.update(
|
110
|
+
marshal_name=lfk,
|
111
|
+
unmarshal_names=[lfk],
|
112
|
+
)
|
113
|
+
else:
|
114
|
+
fo_kw.update(
|
115
|
+
no_marshal=True,
|
116
|
+
no_unmarshal=True,
|
117
|
+
)
|
118
|
+
|
119
|
+
if (lon := field.metadata.get(lm.OBJ_MARSHALER_OMIT_IF_NONE)) is not None:
|
120
|
+
if check.isinstance(lon, bool):
|
121
|
+
fo_kw.update(
|
122
|
+
omit_if=lang.is_none,
|
123
|
+
)
|
124
|
+
|
99
125
|
if fo_kw.get('embed') and not has_set_name:
|
100
126
|
fi_kw.update(
|
101
127
|
marshal_name=fi_kw['marshal_name'] + '_',
|
omlish/sockets/addresses.py
CHANGED
@@ -2,8 +2,7 @@
|
|
2
2
|
# @omlish-lite
|
3
3
|
"""
|
4
4
|
TODO:
|
5
|
-
-
|
6
|
-
+ codification of https://docs.python.org/3/library/socket.html#socket-families
|
5
|
+
- codification of https://docs.python.org/3/library/socket.html#socket-families
|
7
6
|
"""
|
8
7
|
import dataclasses as dc
|
9
8
|
import socket
|
@@ -35,11 +34,16 @@ class SocketAddressInfo:
|
|
35
34
|
sockaddr: SocketAddress
|
36
35
|
|
37
36
|
|
37
|
+
class SocketFamilyAndAddress(ta.NamedTuple):
|
38
|
+
family: socket.AddressFamily
|
39
|
+
address: SocketAddress
|
40
|
+
|
41
|
+
|
38
42
|
def get_best_socket_family(
|
39
43
|
host: ta.Optional[str],
|
40
44
|
port: ta.Union[str, int, None],
|
41
45
|
family: ta.Union[int, socket.AddressFamily] = socket.AddressFamily.AF_UNSPEC,
|
42
|
-
) ->
|
46
|
+
) -> SocketFamilyAndAddress:
|
43
47
|
"""https://github.com/python/cpython/commit/f289084c83190cc72db4a70c58f007ec62e75247"""
|
44
48
|
|
45
49
|
infos = socket.getaddrinfo(
|
@@ -50,4 +54,9 @@ def get_best_socket_family(
|
|
50
54
|
flags=socket.AI_PASSIVE,
|
51
55
|
)
|
52
56
|
ai = SocketAddressInfo(*next(iter(infos)))
|
53
|
-
return ai.family, ai.sockaddr
|
57
|
+
return SocketFamilyAndAddress(ai.family, ai.sockaddr)
|
58
|
+
|
59
|
+
|
60
|
+
class SocketAndAddress(ta.NamedTuple):
|
61
|
+
socket: socket.socket
|
62
|
+
address: SocketAddress
|
omlish/sockets/bind.py
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
"""
|
4
|
+
TODO:
|
5
|
+
- DupSocketBinder
|
6
|
+
"""
|
7
|
+
import abc
|
8
|
+
import dataclasses as dc
|
9
|
+
import errno
|
10
|
+
import os
|
11
|
+
import socket as socket_
|
12
|
+
import stat
|
13
|
+
import typing as ta
|
14
|
+
|
15
|
+
from omlish.lite.check import check
|
16
|
+
from omlish.lite.dataclasses import dataclass_maybe_post_init
|
17
|
+
from omlish.sockets.addresses import SocketAddress
|
18
|
+
from omlish.sockets.addresses import SocketAndAddress
|
19
|
+
|
20
|
+
|
21
|
+
SocketBinderT = ta.TypeVar('SocketBinderT', bound='SocketBinder')
|
22
|
+
SocketBinderConfigT = ta.TypeVar('SocketBinderConfigT', bound='SocketBinder.Config')
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
|
27
|
+
|
28
|
+
class SocketBinder(abc.ABC, ta.Generic[SocketBinderConfigT]):
|
29
|
+
@dc.dataclass(frozen=True)
|
30
|
+
class Config:
|
31
|
+
listen_backlog: int = 5
|
32
|
+
|
33
|
+
allow_reuse_address: bool = True
|
34
|
+
allow_reuse_port: bool = True
|
35
|
+
|
36
|
+
set_inheritable: bool = False
|
37
|
+
|
38
|
+
#
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def new(
|
42
|
+
cls,
|
43
|
+
target: ta.Union[
|
44
|
+
int,
|
45
|
+
ta.Tuple[str, int],
|
46
|
+
str,
|
47
|
+
],
|
48
|
+
) -> 'SocketBinder.Config':
|
49
|
+
if isinstance(target, int):
|
50
|
+
return TcpSocketBinder.Config(
|
51
|
+
port=target,
|
52
|
+
)
|
53
|
+
|
54
|
+
elif isinstance(target, tuple):
|
55
|
+
host, port = target
|
56
|
+
return TcpSocketBinder.Config(
|
57
|
+
host=host,
|
58
|
+
port=port,
|
59
|
+
)
|
60
|
+
|
61
|
+
elif isinstance(target, str):
|
62
|
+
return UnixSocketBinder.Config(
|
63
|
+
file=target,
|
64
|
+
)
|
65
|
+
|
66
|
+
else:
|
67
|
+
raise TypeError(target)
|
68
|
+
|
69
|
+
#
|
70
|
+
|
71
|
+
def __init__(self, config: SocketBinderConfigT) -> None:
|
72
|
+
super().__init__()
|
73
|
+
|
74
|
+
self._config = config
|
75
|
+
|
76
|
+
#
|
77
|
+
|
78
|
+
@classmethod
|
79
|
+
def new(cls, target: ta.Any) -> 'SocketBinder':
|
80
|
+
config: SocketBinder.Config
|
81
|
+
if isinstance(target, SocketBinder.Config):
|
82
|
+
config = target
|
83
|
+
|
84
|
+
else:
|
85
|
+
config = SocketBinder.Config.new(target)
|
86
|
+
|
87
|
+
if isinstance(config, TcpSocketBinder.Config):
|
88
|
+
return TcpSocketBinder(config)
|
89
|
+
|
90
|
+
elif isinstance(config, UnixSocketBinder.Config):
|
91
|
+
return UnixSocketBinder(config)
|
92
|
+
|
93
|
+
else:
|
94
|
+
raise TypeError(config)
|
95
|
+
|
96
|
+
#
|
97
|
+
|
98
|
+
class Error(RuntimeError):
|
99
|
+
pass
|
100
|
+
|
101
|
+
class NotBoundError(Error):
|
102
|
+
pass
|
103
|
+
|
104
|
+
class AlreadyBoundError(Error):
|
105
|
+
pass
|
106
|
+
|
107
|
+
#
|
108
|
+
|
109
|
+
@property
|
110
|
+
@abc.abstractmethod
|
111
|
+
def address_family(self) -> int:
|
112
|
+
raise NotImplementedError
|
113
|
+
|
114
|
+
@property
|
115
|
+
@abc.abstractmethod
|
116
|
+
def address(self) -> SocketAddress:
|
117
|
+
raise NotImplementedError
|
118
|
+
|
119
|
+
#
|
120
|
+
|
121
|
+
_socket: socket_.socket
|
122
|
+
|
123
|
+
@property
|
124
|
+
def is_bound(self) -> bool:
|
125
|
+
return hasattr(self, '_socket')
|
126
|
+
|
127
|
+
@property
|
128
|
+
def socket(self) -> socket_.socket:
|
129
|
+
try:
|
130
|
+
return self._socket
|
131
|
+
except AttributeError:
|
132
|
+
raise self.NotBoundError from None
|
133
|
+
|
134
|
+
_name: str
|
135
|
+
|
136
|
+
@property
|
137
|
+
def name(self) -> str:
|
138
|
+
try:
|
139
|
+
return self._name
|
140
|
+
except AttributeError:
|
141
|
+
raise self.NotBoundError from None
|
142
|
+
|
143
|
+
_port: ta.Optional[int]
|
144
|
+
|
145
|
+
@property
|
146
|
+
def port(self) -> ta.Optional[int]:
|
147
|
+
try:
|
148
|
+
return self._port
|
149
|
+
except AttributeError:
|
150
|
+
raise self.NotBoundError from None
|
151
|
+
|
152
|
+
#
|
153
|
+
|
154
|
+
def fileno(self) -> int:
|
155
|
+
return self.socket.fileno()
|
156
|
+
|
157
|
+
#
|
158
|
+
|
159
|
+
def __enter__(self: SocketBinderT) -> SocketBinderT:
|
160
|
+
self.bind()
|
161
|
+
|
162
|
+
return self
|
163
|
+
|
164
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
165
|
+
self.close()
|
166
|
+
|
167
|
+
#
|
168
|
+
|
169
|
+
def _init_socket(self) -> None:
|
170
|
+
if hasattr(self, '_socket'):
|
171
|
+
raise self.AlreadyBoundError
|
172
|
+
|
173
|
+
socket = socket_.socket(self.address_family, socket_.SOCK_STREAM)
|
174
|
+
self._socket = socket
|
175
|
+
|
176
|
+
if self._config.allow_reuse_address and hasattr(socket_, 'SO_REUSEADDR'):
|
177
|
+
socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEADDR, 1)
|
178
|
+
|
179
|
+
# Since Linux 6.12.9, SO_REUSEPORT is not allowed on other address families than AF_INET/AF_INET6.
|
180
|
+
if (
|
181
|
+
self._config.allow_reuse_port and hasattr(socket_, 'SO_REUSEPORT') and
|
182
|
+
self.address_family in (socket_.AF_INET, socket_.AF_INET6)
|
183
|
+
):
|
184
|
+
try:
|
185
|
+
socket.setsockopt(socket_.SOL_SOCKET, socket_.SO_REUSEPORT, 1)
|
186
|
+
except OSError as err:
|
187
|
+
if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL):
|
188
|
+
raise
|
189
|
+
|
190
|
+
if self._config.set_inheritable and hasattr(socket, 'set_inheritable'):
|
191
|
+
socket.set_inheritable(True)
|
192
|
+
|
193
|
+
def _pre_bind(self) -> None:
|
194
|
+
pass
|
195
|
+
|
196
|
+
def _post_bind(self) -> None:
|
197
|
+
pass
|
198
|
+
|
199
|
+
def bind(self) -> None:
|
200
|
+
self._init_socket()
|
201
|
+
|
202
|
+
self._pre_bind()
|
203
|
+
|
204
|
+
self.socket.bind(self.address)
|
205
|
+
|
206
|
+
self._post_bind()
|
207
|
+
|
208
|
+
check.state(all(hasattr(self, a) for a in ('_socket', '_name', '_port')))
|
209
|
+
|
210
|
+
#
|
211
|
+
|
212
|
+
def close(self) -> None:
|
213
|
+
if hasattr(self, '_socket'):
|
214
|
+
self._socket.close()
|
215
|
+
|
216
|
+
#
|
217
|
+
|
218
|
+
def listen(self) -> None:
|
219
|
+
self.socket.listen(self._config.listen_backlog)
|
220
|
+
|
221
|
+
@abc.abstractmethod
|
222
|
+
def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
|
223
|
+
raise NotImplementedError
|
224
|
+
|
225
|
+
|
226
|
+
##
|
227
|
+
|
228
|
+
|
229
|
+
class TcpSocketBinder(SocketBinder):
|
230
|
+
@dc.dataclass(frozen=True)
|
231
|
+
class Config(SocketBinder.Config):
|
232
|
+
DEFAULT_HOST: ta.ClassVar[str] = 'localhost'
|
233
|
+
host: str = DEFAULT_HOST
|
234
|
+
|
235
|
+
port: int = 0
|
236
|
+
|
237
|
+
def __post_init__(self) -> None:
|
238
|
+
dataclass_maybe_post_init(super())
|
239
|
+
check.non_empty_str(self.host)
|
240
|
+
check.isinstance(self.port, int)
|
241
|
+
check.arg(self.port > 0)
|
242
|
+
|
243
|
+
def __init__(self, config: Config) -> None:
|
244
|
+
super().__init__(check.isinstance(config, self.Config))
|
245
|
+
|
246
|
+
self._address = (config.host, config.port)
|
247
|
+
|
248
|
+
#
|
249
|
+
|
250
|
+
address_family = socket_.AF_INET
|
251
|
+
|
252
|
+
@property
|
253
|
+
def address(self) -> SocketAddress:
|
254
|
+
return self._address
|
255
|
+
|
256
|
+
#
|
257
|
+
|
258
|
+
def _post_bind(self) -> None:
|
259
|
+
super()._post_bind()
|
260
|
+
|
261
|
+
host, port, *_ = self.socket.getsockname()
|
262
|
+
|
263
|
+
self._name = socket_.getfqdn(host)
|
264
|
+
self._port = port
|
265
|
+
|
266
|
+
#
|
267
|
+
|
268
|
+
def accept(self, socket: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
|
269
|
+
if socket is None:
|
270
|
+
socket = self.socket
|
271
|
+
|
272
|
+
conn, client_address = socket.accept()
|
273
|
+
return SocketAndAddress(conn, client_address)
|
274
|
+
|
275
|
+
|
276
|
+
##
|
277
|
+
|
278
|
+
|
279
|
+
class UnixSocketBinder(SocketBinder):
|
280
|
+
@dc.dataclass(frozen=True)
|
281
|
+
class Config(SocketBinder.Config):
|
282
|
+
file: str = ''
|
283
|
+
|
284
|
+
unlink: bool = False
|
285
|
+
|
286
|
+
def __post_init__(self) -> None:
|
287
|
+
dataclass_maybe_post_init(super())
|
288
|
+
check.non_empty_str(self.file)
|
289
|
+
|
290
|
+
def __init__(self, config: Config) -> None:
|
291
|
+
super().__init__(check.isinstance(config, self.Config))
|
292
|
+
|
293
|
+
self._address = config.file
|
294
|
+
|
295
|
+
#
|
296
|
+
|
297
|
+
address_family = socket_.AF_UNIX
|
298
|
+
|
299
|
+
@property
|
300
|
+
def address(self) -> SocketAddress:
|
301
|
+
return self._address
|
302
|
+
|
303
|
+
#
|
304
|
+
|
305
|
+
def _pre_bind(self) -> None:
|
306
|
+
super()._pre_bind()
|
307
|
+
|
308
|
+
if self._config.unlink:
|
309
|
+
try:
|
310
|
+
os.unlink(self._config.file)
|
311
|
+
except FileNotFoundError:
|
312
|
+
pass
|
313
|
+
|
314
|
+
def _post_bind(self) -> None:
|
315
|
+
super()._post_bind()
|
316
|
+
|
317
|
+
name = self.socket.getsockname()
|
318
|
+
|
319
|
+
os.chmod(name, stat.S_IRWXU | stat.S_IRWXG) # noqa
|
320
|
+
|
321
|
+
self._name = name
|
322
|
+
self._port = None
|
323
|
+
|
324
|
+
#
|
325
|
+
|
326
|
+
def accept(self, sock: ta.Optional[socket_.socket] = None) -> SocketAndAddress:
|
327
|
+
if sock is None:
|
328
|
+
sock = self.socket
|
329
|
+
|
330
|
+
conn, _ = sock.accept()
|
331
|
+
client_address = ('', 0)
|
332
|
+
return SocketAndAddress(conn, client_address)
|
omlish/sockets/handlers.py
CHANGED
@@ -1,30 +1,12 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
# @omlish-lite
|
3
|
-
import abc
|
4
3
|
import typing as ta
|
5
4
|
|
6
5
|
from .addresses import SocketAddress
|
6
|
+
from .io import SocketIoPair # noqa
|
7
7
|
|
8
8
|
|
9
|
-
|
9
|
+
SocketHandler = ta.Callable[[SocketAddress, 'SocketIoPair'], None] # ta.TypeAlias
|
10
10
|
|
11
11
|
|
12
12
|
##
|
13
|
-
|
14
|
-
|
15
|
-
class SocketHandler(abc.ABC):
|
16
|
-
def __init__(
|
17
|
-
self,
|
18
|
-
client_address: SocketAddress,
|
19
|
-
rfile: ta.BinaryIO,
|
20
|
-
wfile: ta.BinaryIO,
|
21
|
-
) -> None:
|
22
|
-
super().__init__()
|
23
|
-
|
24
|
-
self._client_address = client_address
|
25
|
-
self._rfile = rfile
|
26
|
-
self._wfile = wfile
|
27
|
-
|
28
|
-
@abc.abstractmethod
|
29
|
-
def handle(self) -> None:
|
30
|
-
raise NotImplementedError
|
omlish/sockets/io.py
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import io
|
4
|
+
import socket
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
|
8
|
+
##
|
9
|
+
|
10
|
+
|
11
|
+
class SocketWriter(io.BufferedIOBase):
|
12
|
+
"""
|
13
|
+
Simple writable BufferedIOBase implementation for a socket
|
14
|
+
|
15
|
+
Does not hold data in a buffer, avoiding any need to call flush().
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, sock):
|
19
|
+
super().__init__()
|
20
|
+
|
21
|
+
self._sock = sock
|
22
|
+
|
23
|
+
def writable(self):
|
24
|
+
return True
|
25
|
+
|
26
|
+
def write(self, b):
|
27
|
+
self._sock.sendall(b)
|
28
|
+
with memoryview(b) as view:
|
29
|
+
return view.nbytes
|
30
|
+
|
31
|
+
def fileno(self):
|
32
|
+
return self._sock.fileno()
|
33
|
+
|
34
|
+
|
35
|
+
class SocketIoPair(ta.NamedTuple):
|
36
|
+
r: ta.BinaryIO
|
37
|
+
w: ta.BinaryIO
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
def from_socket(
|
41
|
+
cls,
|
42
|
+
sock: socket.socket,
|
43
|
+
*,
|
44
|
+
r_buf_size: int = -1,
|
45
|
+
w_buf_size: int = 0,
|
46
|
+
) -> 'SocketIoPair':
|
47
|
+
rf: ta.Any = sock.makefile('rb', r_buf_size)
|
48
|
+
|
49
|
+
if w_buf_size:
|
50
|
+
wf: ta.Any = SocketWriter(sock)
|
51
|
+
else:
|
52
|
+
wf = sock.makefile('wb', w_buf_size)
|
53
|
+
|
54
|
+
return cls(rf, wf)
|
55
|
+
|
56
|
+
|
57
|
+
##
|
58
|
+
|
59
|
+
|
60
|
+
def close_socket_immediately(sock: socket.socket) -> None:
|
61
|
+
try:
|
62
|
+
# Explicitly shutdown. socket.close() merely releases the socket and waits for GC to perform the actual close.
|
63
|
+
sock.shutdown(socket.SHUT_WR)
|
64
|
+
|
65
|
+
except OSError:
|
66
|
+
# Some platforms may raise ENOTCONN here
|
67
|
+
pass
|
68
|
+
|
69
|
+
sock.close()
|
File without changes
|