omlish 0.0.0.dev6__py3-none-any.whl → 0.0.0.dev8__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 +109 -5
- omlish/__init__.py +0 -8
- omlish/asyncs/__init__.py +0 -9
- omlish/asyncs/anyio.py +40 -0
- omlish/bootstrap.py +737 -0
- omlish/check.py +1 -1
- omlish/collections/__init__.py +4 -0
- omlish/collections/exceptions.py +2 -0
- omlish/collections/utils.py +38 -9
- omlish/configs/strings.py +2 -0
- omlish/dataclasses/__init__.py +7 -0
- omlish/dataclasses/impl/descriptors.py +95 -0
- omlish/dataclasses/impl/reflect.py +1 -1
- omlish/dataclasses/utils.py +23 -0
- omlish/{lang/datetimes.py → datetimes.py} +8 -4
- omlish/diag/procfs.py +1 -1
- omlish/diag/threads.py +131 -48
- omlish/docker.py +16 -1
- omlish/fnpairs.py +0 -4
- omlish/{serde → formats}/dotenv.py +3 -0
- omlish/{serde → formats}/yaml.py +2 -2
- omlish/graphs/trees.py +1 -1
- omlish/http/consts.py +6 -0
- omlish/http/sessions.py +2 -2
- omlish/inject/__init__.py +4 -0
- omlish/inject/binder.py +3 -3
- omlish/inject/elements.py +1 -1
- omlish/inject/impl/injector.py +57 -27
- omlish/inject/impl/origins.py +2 -0
- omlish/inject/origins.py +3 -0
- omlish/inject/utils.py +18 -0
- omlish/iterators.py +69 -2
- omlish/lang/__init__.py +16 -7
- omlish/lang/classes/restrict.py +10 -0
- omlish/lang/contextmanagers.py +1 -1
- omlish/lang/descriptors.py +3 -3
- omlish/lang/imports.py +67 -0
- omlish/lang/iterables.py +40 -0
- omlish/lang/maybes.py +3 -0
- omlish/lang/objects.py +38 -0
- omlish/lang/strings.py +25 -0
- omlish/lang/sys.py +9 -0
- omlish/lang/typing.py +37 -0
- omlish/lite/__init__.py +1 -0
- omlish/lite/cached.py +18 -0
- omlish/lite/check.py +29 -0
- omlish/lite/contextmanagers.py +18 -0
- omlish/lite/json.py +30 -0
- omlish/lite/logs.py +121 -0
- omlish/lite/marshal.py +318 -0
- omlish/lite/reflect.py +49 -0
- omlish/lite/runtime.py +18 -0
- omlish/lite/secrets.py +19 -0
- omlish/lite/strings.py +25 -0
- omlish/lite/subprocesses.py +112 -0
- omlish/logs/__init__.py +13 -9
- omlish/logs/configs.py +17 -22
- omlish/logs/formatters.py +3 -48
- omlish/marshal/__init__.py +28 -0
- omlish/marshal/any.py +5 -5
- omlish/marshal/base.py +27 -11
- omlish/marshal/base64.py +24 -9
- omlish/marshal/dataclasses.py +34 -28
- omlish/marshal/datetimes.py +74 -18
- omlish/marshal/enums.py +14 -8
- omlish/marshal/exceptions.py +11 -1
- omlish/marshal/factories.py +59 -74
- omlish/marshal/forbidden.py +35 -0
- omlish/marshal/global_.py +11 -4
- omlish/marshal/iterables.py +21 -24
- omlish/marshal/mappings.py +23 -26
- omlish/marshal/numbers.py +51 -0
- omlish/marshal/optionals.py +11 -12
- omlish/marshal/polymorphism.py +86 -21
- omlish/marshal/primitives.py +4 -5
- omlish/marshal/standard.py +13 -8
- omlish/marshal/uuids.py +4 -5
- omlish/matchfns.py +218 -0
- omlish/os.py +64 -0
- omlish/reflect/__init__.py +39 -0
- omlish/reflect/isinstance.py +38 -0
- omlish/reflect/ops.py +84 -0
- omlish/reflect/subst.py +110 -0
- omlish/reflect/types.py +275 -0
- omlish/secrets/__init__.py +18 -2
- omlish/secrets/crypto.py +132 -0
- omlish/secrets/marshal.py +36 -7
- omlish/secrets/openssl.py +207 -0
- omlish/secrets/secrets.py +260 -8
- omlish/secrets/subprocesses.py +42 -0
- omlish/sql/dbs.py +6 -5
- omlish/sql/exprs.py +12 -0
- omlish/sql/secrets.py +10 -0
- omlish/term.py +1 -1
- omlish/testing/pytest/plugins/switches.py +54 -19
- omlish/text/glyphsplit.py +5 -0
- omlish-0.0.0.dev8.dist-info/METADATA +50 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/RECORD +105 -78
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/WHEEL +1 -1
- omlish/logs/filters.py +0 -11
- omlish/reflect.py +0 -470
- omlish-0.0.0.dev6.dist-info/METADATA +0 -34
- /omlish/{asyncs/futures.py → concurrent.py} +0 -0
- /omlish/{serde → formats}/__init__.py +0 -0
- /omlish/{serde → formats}/json.py +0 -0
- /omlish/{serde → formats}/props.py +0 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev6.dist-info → omlish-0.0.0.dev8.dist-info}/top_level.txt +0 -0
omlish/bootstrap.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- more logging options
|
|
4
|
+
- a more powerful interface would be run_fn_with_bootstrap..
|
|
5
|
+
|
|
6
|
+
TODO new items:
|
|
7
|
+
- pydevd connect-back
|
|
8
|
+
- debugging / pdb
|
|
9
|
+
- repl server
|
|
10
|
+
- packaging fixups
|
|
11
|
+
- daemonize ( https://github.com/thesharp/daemonize/blob/master/daemonize.py )
|
|
12
|
+
"""
|
|
13
|
+
# ruff: noqa: UP006 UP007
|
|
14
|
+
import abc
|
|
15
|
+
import argparse
|
|
16
|
+
import contextlib
|
|
17
|
+
import dataclasses as dc
|
|
18
|
+
import enum
|
|
19
|
+
import faulthandler
|
|
20
|
+
import gc
|
|
21
|
+
import importlib
|
|
22
|
+
import io
|
|
23
|
+
import itertools
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import pwd
|
|
27
|
+
import resource
|
|
28
|
+
import signal
|
|
29
|
+
import sys
|
|
30
|
+
import typing as ta
|
|
31
|
+
|
|
32
|
+
from . import lang
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if ta.TYPE_CHECKING:
|
|
36
|
+
import cProfile # noqa
|
|
37
|
+
import pstats
|
|
38
|
+
import runpy
|
|
39
|
+
|
|
40
|
+
from . import libc
|
|
41
|
+
from . import logs
|
|
42
|
+
from . import os as osu
|
|
43
|
+
from .diag import threads as diagt
|
|
44
|
+
from .formats import dotenv
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
cProfile = lang.proxy_import('cProfile') # noqa
|
|
48
|
+
pstats = lang.proxy_import('pstats')
|
|
49
|
+
runpy = lang.proxy_import('runpy')
|
|
50
|
+
|
|
51
|
+
libc = lang.proxy_import('.libc', __package__)
|
|
52
|
+
logs = lang.proxy_import('.logs', __package__)
|
|
53
|
+
osu = lang.proxy_import('.os', __package__)
|
|
54
|
+
diagt = lang.proxy_import('.diag.threads', __package__)
|
|
55
|
+
dotenv = lang.proxy_import('.formats.dotenv', __package__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
BootstrapConfigT = ta.TypeVar('BootstrapConfigT', bound='Bootstrap.Config')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Bootstrap(abc.ABC, ta.Generic[BootstrapConfigT]):
|
|
65
|
+
@dc.dataclass(frozen=True)
|
|
66
|
+
class Config(abc.ABC): # noqa
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def __init__(self, config: BootstrapConfigT) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
self._config = config
|
|
72
|
+
|
|
73
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
|
74
|
+
super().__init_subclass__(**kwargs)
|
|
75
|
+
if not cls.__name__.endswith('Bootstrap'):
|
|
76
|
+
raise NameError(cls)
|
|
77
|
+
if abc.ABC not in cls.__bases__ and not issubclass(cls.__dict__['Config'], Bootstrap.Config):
|
|
78
|
+
raise TypeError(cls)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SimpleBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
|
|
82
|
+
@abc.abstractmethod
|
|
83
|
+
def run(self) -> None:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ContextBootstrap(Bootstrap[BootstrapConfigT], abc.ABC):
|
|
88
|
+
@abc.abstractmethod
|
|
89
|
+
def enter(self) -> ta.ContextManager[None]:
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CwdBootstrap(ContextBootstrap['CwdBootstrap.Config']):
|
|
97
|
+
@dc.dataclass(frozen=True)
|
|
98
|
+
class Config(Bootstrap.Config):
|
|
99
|
+
path: ta.Optional[str] = None
|
|
100
|
+
|
|
101
|
+
@contextlib.contextmanager
|
|
102
|
+
def enter(self) -> ta.Iterator[None]:
|
|
103
|
+
if self._config.path is not None:
|
|
104
|
+
prev = os.getcwd()
|
|
105
|
+
os.chdir(self._config.path)
|
|
106
|
+
else:
|
|
107
|
+
prev = None
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
|
|
112
|
+
finally:
|
|
113
|
+
if prev is not None:
|
|
114
|
+
os.chdir(prev)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SetuidBootstrap(SimpleBootstrap['SetuidBootstrap.Config']):
|
|
121
|
+
@dc.dataclass(frozen=True)
|
|
122
|
+
class Config(Bootstrap.Config):
|
|
123
|
+
user: ta.Optional[str] = None
|
|
124
|
+
|
|
125
|
+
def run(self) -> None:
|
|
126
|
+
if self._config.user is not None:
|
|
127
|
+
user = pwd.getpwnam(self._config.user)
|
|
128
|
+
os.setuid(user.pw_uid)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class GcDebugFlag(enum.Enum):
|
|
135
|
+
STATS = gc.DEBUG_STATS
|
|
136
|
+
COLLECTABLE = gc.DEBUG_COLLECTABLE
|
|
137
|
+
UNCOLLECTABLE = gc.DEBUG_UNCOLLECTABLE
|
|
138
|
+
SAVEALL = gc.DEBUG_SAVEALL
|
|
139
|
+
LEAK = gc.DEBUG_LEAK
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class GcBootstrap(ContextBootstrap['GcBootstrap.Config']):
|
|
143
|
+
@dc.dataclass(frozen=True)
|
|
144
|
+
class Config(Bootstrap.Config):
|
|
145
|
+
disable: bool = False
|
|
146
|
+
debug: ta.Optional[int] = None
|
|
147
|
+
|
|
148
|
+
@contextlib.contextmanager
|
|
149
|
+
def enter(self) -> ta.Iterator[None]:
|
|
150
|
+
prev_enabled = gc.isenabled()
|
|
151
|
+
if self._config.disable:
|
|
152
|
+
gc.disable()
|
|
153
|
+
|
|
154
|
+
if self._config.debug is not None:
|
|
155
|
+
prev_debug = gc.get_debug()
|
|
156
|
+
gc.set_debug(self._config.debug)
|
|
157
|
+
else:
|
|
158
|
+
prev_debug = None
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
yield
|
|
162
|
+
|
|
163
|
+
finally:
|
|
164
|
+
if prev_enabled:
|
|
165
|
+
gc.enable()
|
|
166
|
+
|
|
167
|
+
if prev_debug is not None:
|
|
168
|
+
gc.set_debug(prev_debug)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class NiceBootstrap(SimpleBootstrap['NiceBootstrap.Config']):
|
|
175
|
+
@dc.dataclass(frozen=True)
|
|
176
|
+
class Config(Bootstrap.Config):
|
|
177
|
+
nice: ta.Optional[int] = None
|
|
178
|
+
|
|
179
|
+
def run(self) -> None:
|
|
180
|
+
if self._config.nice is not None:
|
|
181
|
+
os.nice(self._config.nice)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class LogBootstrap(ContextBootstrap['LogBootstrap.Config']):
|
|
188
|
+
@dc.dataclass(frozen=True)
|
|
189
|
+
class Config(Bootstrap.Config):
|
|
190
|
+
level: ta.Union[str, int, None] = None
|
|
191
|
+
json: bool = False
|
|
192
|
+
|
|
193
|
+
@contextlib.contextmanager
|
|
194
|
+
def enter(self) -> ta.Iterator[None]:
|
|
195
|
+
if self._config.level is None:
|
|
196
|
+
yield
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
handler = logs.configure_standard_logging(
|
|
200
|
+
self._config.level,
|
|
201
|
+
json=self._config.json,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
yield
|
|
206
|
+
|
|
207
|
+
finally:
|
|
208
|
+
if handler is not None:
|
|
209
|
+
logging.root.removeHandler(handler)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class FaulthandlerBootstrap(ContextBootstrap['FaulthandlerBootstrap.Config']):
|
|
216
|
+
@dc.dataclass(frozen=True)
|
|
217
|
+
class Config(Bootstrap.Config):
|
|
218
|
+
enabled: ta.Optional[bool] = None
|
|
219
|
+
|
|
220
|
+
@contextlib.contextmanager
|
|
221
|
+
def enter(self) -> ta.Iterator[None]:
|
|
222
|
+
if self._config.enabled is None:
|
|
223
|
+
yield
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
prev = faulthandler.is_enabled()
|
|
227
|
+
if self._config.enabled:
|
|
228
|
+
faulthandler.enable()
|
|
229
|
+
else:
|
|
230
|
+
faulthandler.disable()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
yield
|
|
234
|
+
|
|
235
|
+
finally:
|
|
236
|
+
if prev:
|
|
237
|
+
faulthandler.enable()
|
|
238
|
+
else:
|
|
239
|
+
faulthandler.disable()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
##
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
SIGNALS_BY_NAME = {
|
|
246
|
+
a[len('SIG'):]: v # noqa
|
|
247
|
+
for a in dir(signal)
|
|
248
|
+
if a.startswith('SIG')
|
|
249
|
+
and not a.startswith('SIG_')
|
|
250
|
+
and a == a.upper()
|
|
251
|
+
and isinstance((v := getattr(signal, a)), int)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class PrctlBootstrap(SimpleBootstrap['PrctlBootstrap.Config']):
|
|
256
|
+
@dc.dataclass(frozen=True)
|
|
257
|
+
class Config(Bootstrap.Config):
|
|
258
|
+
dumpable: bool = False
|
|
259
|
+
deathsig: ta.Union[int, str, None] = None
|
|
260
|
+
|
|
261
|
+
def run(self) -> None:
|
|
262
|
+
if self._config.dumpable:
|
|
263
|
+
libc.prctl(libc.PR_SET_DUMPABLE, 1, 0, 0, 0, 0)
|
|
264
|
+
|
|
265
|
+
if self._config.deathsig is not None:
|
|
266
|
+
if isinstance(self._config.deathsig, int):
|
|
267
|
+
sig = self._config.deathsig
|
|
268
|
+
else:
|
|
269
|
+
sig = SIGNALS_BY_NAME[self._config.deathsig.upper()]
|
|
270
|
+
libc.prctl(libc.PR_SET_PDEATHSIG, sig, 0, 0, 0, 0)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
##
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
RLIMITS_BY_NAME = {
|
|
277
|
+
a[len('RLIMIT_'):]: v # noqa
|
|
278
|
+
for a in dir(resource)
|
|
279
|
+
if a.startswith('RLIMIT_')
|
|
280
|
+
and a == a.upper()
|
|
281
|
+
and isinstance((v := getattr(resource, a)), int)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class RlimitBootstrap(ContextBootstrap['RlimitBootstrap.Config']):
|
|
286
|
+
@dc.dataclass(frozen=True)
|
|
287
|
+
class Config(Bootstrap.Config):
|
|
288
|
+
limits: ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]] = None
|
|
289
|
+
|
|
290
|
+
@contextlib.contextmanager
|
|
291
|
+
def enter(self) -> ta.Iterator[None]:
|
|
292
|
+
if not self._config.limits:
|
|
293
|
+
yield
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
def or_infin(l: ta.Optional[int]) -> int:
|
|
297
|
+
return l if l is not None else resource.RLIM_INFINITY
|
|
298
|
+
|
|
299
|
+
prev = {}
|
|
300
|
+
for k, (s, h) in self._config.limits.items():
|
|
301
|
+
i = RLIMITS_BY_NAME[k.upper()]
|
|
302
|
+
prev[i] = resource.getrlimit(i)
|
|
303
|
+
resource.setrlimit(i, (or_infin(s), or_infin(h)))
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
yield
|
|
307
|
+
|
|
308
|
+
finally:
|
|
309
|
+
for i, (s, h) in prev.items():
|
|
310
|
+
resource.setrlimit(i, (s, h))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ImportBootstrap(SimpleBootstrap['ImportBootstrap.Config']):
|
|
317
|
+
@dc.dataclass(frozen=True)
|
|
318
|
+
class Config(Bootstrap.Config):
|
|
319
|
+
modules: ta.Optional[ta.Sequence[str]] = None
|
|
320
|
+
|
|
321
|
+
def run(self) -> None:
|
|
322
|
+
for m in self._config.modules or ():
|
|
323
|
+
importlib.import_module(m)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
##
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class ProfilingBootstrap(ContextBootstrap['ProfilingBootstrap.Config']):
|
|
330
|
+
@dc.dataclass(frozen=True)
|
|
331
|
+
class Config(Bootstrap.Config):
|
|
332
|
+
enable: bool = False
|
|
333
|
+
builtins: bool = True
|
|
334
|
+
|
|
335
|
+
outfile: ta.Optional[str] = None
|
|
336
|
+
|
|
337
|
+
print: bool = False
|
|
338
|
+
sort: str = 'cumtime'
|
|
339
|
+
topn: int = 100
|
|
340
|
+
|
|
341
|
+
@contextlib.contextmanager
|
|
342
|
+
def enter(self) -> ta.Iterator[None]:
|
|
343
|
+
if not self._config.enable:
|
|
344
|
+
yield
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
prof = cProfile.Profile()
|
|
348
|
+
prof.enable()
|
|
349
|
+
try:
|
|
350
|
+
yield
|
|
351
|
+
|
|
352
|
+
finally:
|
|
353
|
+
prof.disable()
|
|
354
|
+
prof.create_stats()
|
|
355
|
+
|
|
356
|
+
if self._config.print:
|
|
357
|
+
pstats.Stats(prof) \
|
|
358
|
+
.strip_dirs() \
|
|
359
|
+
.sort_stats(self._config.sort) \
|
|
360
|
+
.print_stats(self._config.topn)
|
|
361
|
+
|
|
362
|
+
if self._config.outfile is not None:
|
|
363
|
+
prof.dump_stats(self._config.outfile)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
##
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class EnvBootstrap(ContextBootstrap['EnvBootstrap.Config']):
|
|
370
|
+
@dc.dataclass(frozen=True)
|
|
371
|
+
class Config(Bootstrap.Config):
|
|
372
|
+
vars: ta.Optional[ta.Mapping[str, ta.Optional[str]]] = None
|
|
373
|
+
files: ta.Optional[ta.Sequence[str]] = None
|
|
374
|
+
|
|
375
|
+
@contextlib.contextmanager
|
|
376
|
+
def enter(self) -> ta.Iterator[None]:
|
|
377
|
+
if not (self._config.vars or self._config.files):
|
|
378
|
+
yield
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
new = dict(self._config.vars or {})
|
|
382
|
+
for f in self._config.files or ():
|
|
383
|
+
new.update(dotenv.dotenv_values(f, env=os.environ))
|
|
384
|
+
|
|
385
|
+
prev: ta.Dict[str, ta.Optional[str]] = {k: os.environ.get(k) for k in new}
|
|
386
|
+
|
|
387
|
+
def do(k: str, v: ta.Optional[str]) -> None:
|
|
388
|
+
if v is not None:
|
|
389
|
+
os.environ[k] = v
|
|
390
|
+
else:
|
|
391
|
+
del os.environ[k]
|
|
392
|
+
|
|
393
|
+
for k, v in new.items():
|
|
394
|
+
do(k, v)
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
yield
|
|
398
|
+
|
|
399
|
+
finally:
|
|
400
|
+
for k, v in prev.items():
|
|
401
|
+
do(k, v)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
##
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class ThreadDumpBootstrap(ContextBootstrap['ThreadDumpBootstrap.Config']):
|
|
408
|
+
@dc.dataclass(frozen=True)
|
|
409
|
+
class Config(Bootstrap.Config):
|
|
410
|
+
interval_s: ta.Optional[float] = None
|
|
411
|
+
|
|
412
|
+
on_sigquit: bool = False
|
|
413
|
+
|
|
414
|
+
@contextlib.contextmanager
|
|
415
|
+
def enter(self) -> ta.Iterator[None]:
|
|
416
|
+
if self._config.interval_s:
|
|
417
|
+
tdt = diagt.create_thread_dump_thread(
|
|
418
|
+
interval_s=self._config.interval_s,
|
|
419
|
+
start=True,
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
tdt = None
|
|
423
|
+
|
|
424
|
+
if self._config.on_sigquit:
|
|
425
|
+
dump_threads_str = diagt.dump_threads_str
|
|
426
|
+
|
|
427
|
+
def handler(signum, frame):
|
|
428
|
+
print(dump_threads_str(), file=sys.stderr)
|
|
429
|
+
|
|
430
|
+
prev_sq = lang.just(signal.signal(signal.SIGQUIT, handler))
|
|
431
|
+
else:
|
|
432
|
+
prev_sq = lang.empty()
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
yield
|
|
436
|
+
|
|
437
|
+
finally:
|
|
438
|
+
if tdt is not None:
|
|
439
|
+
tdt.stop_nowait()
|
|
440
|
+
|
|
441
|
+
if prev_sq.present:
|
|
442
|
+
signal.signal(signal.SIGQUIT, prev_sq.must())
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
##
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class TimebombBootstrap(ContextBootstrap['TimebombBootstrap.Config']):
|
|
449
|
+
@dc.dataclass(frozen=True)
|
|
450
|
+
class Config(Bootstrap.Config):
|
|
451
|
+
delay_s: ta.Optional[float] = None
|
|
452
|
+
|
|
453
|
+
@contextlib.contextmanager
|
|
454
|
+
def enter(self) -> ta.Iterator[None]:
|
|
455
|
+
if not self._config.delay_s:
|
|
456
|
+
yield
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
tbt = diagt.create_timebomb_thread(
|
|
460
|
+
self._config.delay_s,
|
|
461
|
+
start=True,
|
|
462
|
+
)
|
|
463
|
+
try:
|
|
464
|
+
yield
|
|
465
|
+
finally:
|
|
466
|
+
tbt.stop_nowait()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
##
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class PidfileBootstrap(ContextBootstrap['PidfileBootstrap.Config']):
|
|
473
|
+
@dc.dataclass(frozen=True)
|
|
474
|
+
class Config(Bootstrap.Config):
|
|
475
|
+
path: ta.Optional[str] = None
|
|
476
|
+
|
|
477
|
+
@contextlib.contextmanager
|
|
478
|
+
def enter(self) -> ta.Iterator[None]:
|
|
479
|
+
if self._config.path is None:
|
|
480
|
+
yield
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
with osu.Pidfile(self._config.path) as pf:
|
|
484
|
+
pf.write()
|
|
485
|
+
yield
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
##
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class FdsBootstrap(SimpleBootstrap['FdsBootstrap.Config']):
|
|
492
|
+
@dc.dataclass(frozen=True)
|
|
493
|
+
class Config(Bootstrap.Config):
|
|
494
|
+
redirects: ta.Optional[ta.Mapping[int, ta.Union[int, str, None]]] = None
|
|
495
|
+
|
|
496
|
+
def run(self) -> None:
|
|
497
|
+
for dst, src in (self._config.redirects or {}).items():
|
|
498
|
+
if src is None:
|
|
499
|
+
src = '/dev/null'
|
|
500
|
+
if isinstance(src, int):
|
|
501
|
+
os.dup2(src, dst)
|
|
502
|
+
elif isinstance(src, str):
|
|
503
|
+
sfd = os.open(src, os.O_RDWR)
|
|
504
|
+
os.dup2(sfd, dst)
|
|
505
|
+
os.close(sfd)
|
|
506
|
+
else:
|
|
507
|
+
raise TypeError(src)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
##
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class PrintPidBootstrap(SimpleBootstrap['PrintPidBootstrap.Config']):
|
|
514
|
+
@dc.dataclass(frozen=True)
|
|
515
|
+
class Config(Bootstrap.Config):
|
|
516
|
+
enable: bool = False
|
|
517
|
+
pause: bool = False
|
|
518
|
+
|
|
519
|
+
def run(self) -> None:
|
|
520
|
+
if not (self._config.enable or self._config.pause):
|
|
521
|
+
return
|
|
522
|
+
print(str(os.getpid()), file=sys.stderr)
|
|
523
|
+
if self._config.pause:
|
|
524
|
+
input()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
##
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
BOOTSTRAP_TYPES_BY_NAME: ta.Mapping[str, ta.Type[Bootstrap]] = { # noqa
|
|
531
|
+
lang.snake_case(cls.__name__[:-len('Bootstrap')]): cls
|
|
532
|
+
for cls in lang.deep_subclasses(Bootstrap)
|
|
533
|
+
if not lang.is_abstract_class(cls)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
BOOTSTRAP_TYPES_BY_CONFIG_TYPE: ta.Mapping[ta.Type[Bootstrap.Config], ta.Type[Bootstrap]] = {
|
|
537
|
+
cls.Config: cls
|
|
538
|
+
for cls in BOOTSTRAP_TYPES_BY_NAME.values()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
##
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class BootstrapHarness:
|
|
546
|
+
def __init__(self, lst: ta.Sequence[Bootstrap]) -> None:
|
|
547
|
+
super().__init__()
|
|
548
|
+
self._lst = lst
|
|
549
|
+
|
|
550
|
+
@contextlib.contextmanager
|
|
551
|
+
def __call__(self) -> ta.Iterator[None]:
|
|
552
|
+
with contextlib.ExitStack() as es:
|
|
553
|
+
for c in self._lst:
|
|
554
|
+
if isinstance(c, SimpleBootstrap):
|
|
555
|
+
c.run()
|
|
556
|
+
elif isinstance(c, ContextBootstrap):
|
|
557
|
+
es.enter_context(c.enter())
|
|
558
|
+
else:
|
|
559
|
+
raise TypeError(c)
|
|
560
|
+
|
|
561
|
+
yield
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
##
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@contextlib.contextmanager
|
|
568
|
+
def bootstrap(*cfgs: Bootstrap.Config) -> ta.Iterator[None]:
|
|
569
|
+
with BootstrapHarness([
|
|
570
|
+
BOOTSTRAP_TYPES_BY_CONFIG_TYPE[type(c)](c)
|
|
571
|
+
for c in cfgs
|
|
572
|
+
])():
|
|
573
|
+
yield
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
##
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class _OrderedArgsAction(argparse.Action):
|
|
580
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
581
|
+
if 'ordered_args' not in namespace:
|
|
582
|
+
setattr(namespace, 'ordered_args', [])
|
|
583
|
+
if self.const is not None:
|
|
584
|
+
value = self.const
|
|
585
|
+
else:
|
|
586
|
+
value = values
|
|
587
|
+
namespace.ordered_args.append((self.dest, value))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _or_opt(ty):
|
|
591
|
+
return (ty, ta.Optional[ty])
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _int_or_str(v):
|
|
595
|
+
try:
|
|
596
|
+
return int(v)
|
|
597
|
+
except ValueError:
|
|
598
|
+
return v
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
602
|
+
# ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]]
|
|
603
|
+
|
|
604
|
+
for cname, cls in BOOTSTRAP_TYPES_BY_NAME.items():
|
|
605
|
+
for fld in dc.fields(cls.Config):
|
|
606
|
+
aname = f'--{cname}:{fld.name}'
|
|
607
|
+
kw: ta.Dict[str, ta.Any] = {}
|
|
608
|
+
|
|
609
|
+
if fld.type in _or_opt(str):
|
|
610
|
+
pass
|
|
611
|
+
elif fld.type in _or_opt(bool):
|
|
612
|
+
kw.update(const=True, nargs=0)
|
|
613
|
+
elif fld.type in _or_opt(int):
|
|
614
|
+
kw.update(type=int)
|
|
615
|
+
elif fld.type in _or_opt(float):
|
|
616
|
+
kw.update(type=float)
|
|
617
|
+
elif fld.type in _or_opt(ta.Union[int, str]):
|
|
618
|
+
kw.update(type=_int_or_str)
|
|
619
|
+
|
|
620
|
+
elif fld.type in (
|
|
621
|
+
*_or_opt(ta.Sequence[str]),
|
|
622
|
+
*_or_opt(ta.Mapping[str, ta.Optional[str]]),
|
|
623
|
+
*_or_opt(ta.Mapping[int, ta.Union[int, str, None]]),
|
|
624
|
+
*_or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]),
|
|
625
|
+
):
|
|
626
|
+
if aname[-1] != 's':
|
|
627
|
+
raise NameError(aname)
|
|
628
|
+
aname = aname[:-1]
|
|
629
|
+
|
|
630
|
+
else:
|
|
631
|
+
raise TypeError(fld)
|
|
632
|
+
|
|
633
|
+
parser.add_argument(aname, action=_OrderedArgsAction, **kw)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _process_arguments(args: ta.Any) -> ta.Sequence[Bootstrap.Config]:
|
|
637
|
+
if not (oa := getattr(args, 'ordered_args', None)):
|
|
638
|
+
return []
|
|
639
|
+
|
|
640
|
+
cfgs: ta.List[Bootstrap.Config] = []
|
|
641
|
+
|
|
642
|
+
for cname, cargs in [
|
|
643
|
+
(n, list(g))
|
|
644
|
+
for n, g in
|
|
645
|
+
itertools.groupby(oa, key=lambda s: s[0].partition(':')[0])
|
|
646
|
+
]:
|
|
647
|
+
ccls = BOOTSTRAP_TYPES_BY_NAME[cname].Config
|
|
648
|
+
flds = {f.name: f for f in dc.fields(ccls)}
|
|
649
|
+
|
|
650
|
+
kw: ta.Dict[str, ta.Any] = {}
|
|
651
|
+
for aname, aval in cargs:
|
|
652
|
+
k = aname.partition(':')[2]
|
|
653
|
+
|
|
654
|
+
if k not in flds:
|
|
655
|
+
k += 's'
|
|
656
|
+
fld = flds[k]
|
|
657
|
+
|
|
658
|
+
if fld.type in _or_opt(ta.Sequence[str]):
|
|
659
|
+
kw.setdefault(k, []).append(aval)
|
|
660
|
+
|
|
661
|
+
elif fld.type in _or_opt(ta.Mapping[str, ta.Optional[str]]):
|
|
662
|
+
if '=' not in aval:
|
|
663
|
+
kw.setdefault(k, {})[aval] = None
|
|
664
|
+
else:
|
|
665
|
+
ek, _, ev = aval.partition('=')
|
|
666
|
+
kw.setdefault(k, {})[ek] = ev
|
|
667
|
+
|
|
668
|
+
elif fld.type in _or_opt(ta.Mapping[int, ta.Union[int, str, None]]):
|
|
669
|
+
fk, _, fv = aval.partition('=')
|
|
670
|
+
if not fv:
|
|
671
|
+
kw.setdefault(k, {})[int(fk)] = None
|
|
672
|
+
else:
|
|
673
|
+
kw.setdefault(k, {})[int(fk)] = _int_or_str(fv)
|
|
674
|
+
|
|
675
|
+
elif fld.type in _or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]):
|
|
676
|
+
fk, _, fv = aval.partition('=')
|
|
677
|
+
if ',' in fv:
|
|
678
|
+
tl, tr = fv.split(',')
|
|
679
|
+
else:
|
|
680
|
+
tl, tr = None, None
|
|
681
|
+
kw.setdefault(k, {})[fk] = (_int_or_str(tl) if tl else None, _int_or_str(tr) if tr else None)
|
|
682
|
+
|
|
683
|
+
else:
|
|
684
|
+
raise TypeError(fld)
|
|
685
|
+
|
|
686
|
+
else:
|
|
687
|
+
kw[k] = aval
|
|
688
|
+
|
|
689
|
+
cfg = ccls(**kw)
|
|
690
|
+
cfgs.append(cfg)
|
|
691
|
+
|
|
692
|
+
return cfgs
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
##
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _main() -> int:
|
|
699
|
+
parser = argparse.ArgumentParser()
|
|
700
|
+
|
|
701
|
+
_add_arguments(parser)
|
|
702
|
+
|
|
703
|
+
parser.add_argument('-m', '--module', action='store_true')
|
|
704
|
+
parser.add_argument('target')
|
|
705
|
+
parser.add_argument('args', nargs=argparse.REMAINDER)
|
|
706
|
+
|
|
707
|
+
args = parser.parse_args()
|
|
708
|
+
cfgs = _process_arguments(args)
|
|
709
|
+
|
|
710
|
+
with bootstrap(*cfgs):
|
|
711
|
+
tgt = args.target
|
|
712
|
+
|
|
713
|
+
if args.module:
|
|
714
|
+
sys.argv = [tgt, *(args.args or ())]
|
|
715
|
+
runpy._run_module_as_main(tgt) # type: ignore # noqa
|
|
716
|
+
|
|
717
|
+
else:
|
|
718
|
+
with io.open_code(tgt) as fp:
|
|
719
|
+
src = fp.read()
|
|
720
|
+
|
|
721
|
+
ns = dict(
|
|
722
|
+
__name__='__main__',
|
|
723
|
+
__file__=tgt,
|
|
724
|
+
__builtins__=__builtins__,
|
|
725
|
+
__spec__=None,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
import __main__ # noqa
|
|
729
|
+
__main__.__dict__.clear()
|
|
730
|
+
__main__.__dict__.update(ns)
|
|
731
|
+
exec(compile(src, tgt, 'exec'), __main__.__dict__, __main__.__dict__)
|
|
732
|
+
|
|
733
|
+
return 0
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
if __name__ == '__main__':
|
|
737
|
+
sys.exit(_main())
|