ominfra 0.0.0.dev119__py3-none-any.whl → 0.0.0.dev121__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/clouds/aws/journald2aws/__main__.py +4 -0
- ominfra/clouds/aws/journald2aws/driver.py +34 -13
- ominfra/clouds/aws/journald2aws/main.py +2 -5
- ominfra/configs.py +70 -0
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +994 -26
- ominfra/scripts/supervisor.py +1969 -262
- ominfra/supervisor/compat.py +13 -0
- ominfra/supervisor/configs.py +21 -0
- ominfra/supervisor/context.py +13 -2
- ominfra/supervisor/dispatchers.py +4 -4
- ominfra/supervisor/events.py +4 -7
- ominfra/supervisor/main.py +82 -11
- ominfra/supervisor/process.py +46 -10
- ominfra/supervisor/supervisor.py +118 -88
- ominfra/supervisor/types.py +5 -0
- ominfra/threadworkers.py +66 -9
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/RECORD +25 -23
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -18,10 +18,12 @@ import fcntl
|
|
18
18
|
import fractions
|
19
19
|
import functools
|
20
20
|
import grp
|
21
|
+
import inspect
|
21
22
|
import itertools
|
22
23
|
import json
|
23
24
|
import logging
|
24
25
|
import os
|
26
|
+
import os.path
|
25
27
|
import pwd
|
26
28
|
import re
|
27
29
|
import resource
|
@@ -29,6 +31,7 @@ import select
|
|
29
31
|
import shlex
|
30
32
|
import signal
|
31
33
|
import stat
|
34
|
+
import string
|
32
35
|
import sys
|
33
36
|
import syslog
|
34
37
|
import tempfile
|
@@ -53,6 +56,11 @@ if sys.version_info < (3, 8):
|
|
53
56
|
########################################
|
54
57
|
|
55
58
|
|
59
|
+
# ../../../omdev/toml/parser.py
|
60
|
+
TomlParseFloat = ta.Callable[[str], ta.Any]
|
61
|
+
TomlKey = ta.Tuple[str, ...]
|
62
|
+
TomlPos = int # ta.TypeAlias
|
63
|
+
|
56
64
|
# ../compat.py
|
57
65
|
T = ta.TypeVar('T')
|
58
66
|
|
@@ -60,6 +68,837 @@ T = ta.TypeVar('T')
|
|
60
68
|
ProcessState = int # ta.TypeAlias
|
61
69
|
SupervisorState = int # ta.TypeAlias
|
62
70
|
|
71
|
+
# ../../../omlish/lite/inject.py
|
72
|
+
InjectorKeyCls = ta.Union[type, ta.NewType]
|
73
|
+
InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
|
74
|
+
InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
|
75
|
+
InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
|
76
|
+
|
77
|
+
# ../../configs.py
|
78
|
+
ConfigMapping = ta.Mapping[str, ta.Any]
|
79
|
+
|
80
|
+
# ../context.py
|
81
|
+
ServerEpoch = ta.NewType('ServerEpoch', int)
|
82
|
+
InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
|
83
|
+
|
84
|
+
|
85
|
+
########################################
|
86
|
+
# ../../../omdev/toml/parser.py
|
87
|
+
# SPDX-License-Identifier: MIT
|
88
|
+
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
89
|
+
# Licensed to PSF under a Contributor Agreement.
|
90
|
+
#
|
91
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
92
|
+
# --------------------------------------------
|
93
|
+
#
|
94
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
95
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
96
|
+
# documentation.
|
97
|
+
#
|
98
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
99
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
100
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
101
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
102
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
|
103
|
+
# Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
|
104
|
+
#
|
105
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
106
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
107
|
+
# any such work a brief summary of the changes made to Python.
|
108
|
+
#
|
109
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
110
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
111
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
112
|
+
# RIGHTS.
|
113
|
+
#
|
114
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
115
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
116
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
117
|
+
#
|
118
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
119
|
+
#
|
120
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
121
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
122
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
123
|
+
#
|
124
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
125
|
+
# License Agreement.
|
126
|
+
#
|
127
|
+
# https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
|
128
|
+
|
129
|
+
|
130
|
+
##
|
131
|
+
|
132
|
+
|
133
|
+
_TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
|
134
|
+
|
135
|
+
TOML_RE_NUMBER = re.compile(
|
136
|
+
r"""
|
137
|
+
0
|
138
|
+
(?:
|
139
|
+
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
140
|
+
|
|
141
|
+
b[01](?:_?[01])* # bin
|
142
|
+
|
|
143
|
+
o[0-7](?:_?[0-7])* # oct
|
144
|
+
)
|
145
|
+
|
|
146
|
+
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
147
|
+
(?P<floatpart>
|
148
|
+
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
149
|
+
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
150
|
+
)
|
151
|
+
""",
|
152
|
+
flags=re.VERBOSE,
|
153
|
+
)
|
154
|
+
TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
|
155
|
+
TOML_RE_DATETIME = re.compile(
|
156
|
+
rf"""
|
157
|
+
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
158
|
+
(?:
|
159
|
+
[Tt ]
|
160
|
+
{_TOML_TIME_RE_STR}
|
161
|
+
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
162
|
+
)?
|
163
|
+
""",
|
164
|
+
flags=re.VERBOSE,
|
165
|
+
)
|
166
|
+
|
167
|
+
|
168
|
+
def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
|
169
|
+
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
170
|
+
|
171
|
+
Raises ValueError if the match does not correspond to a valid date or datetime.
|
172
|
+
"""
|
173
|
+
(
|
174
|
+
year_str,
|
175
|
+
month_str,
|
176
|
+
day_str,
|
177
|
+
hour_str,
|
178
|
+
minute_str,
|
179
|
+
sec_str,
|
180
|
+
micros_str,
|
181
|
+
zulu_time,
|
182
|
+
offset_sign_str,
|
183
|
+
offset_hour_str,
|
184
|
+
offset_minute_str,
|
185
|
+
) = match.groups()
|
186
|
+
year, month, day = int(year_str), int(month_str), int(day_str)
|
187
|
+
if hour_str is None:
|
188
|
+
return datetime.date(year, month, day)
|
189
|
+
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
190
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
191
|
+
if offset_sign_str:
|
192
|
+
tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
|
193
|
+
offset_hour_str, offset_minute_str, offset_sign_str,
|
194
|
+
)
|
195
|
+
elif zulu_time:
|
196
|
+
tz = datetime.UTC
|
197
|
+
else: # local date-time
|
198
|
+
tz = None
|
199
|
+
return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
200
|
+
|
201
|
+
|
202
|
+
@functools.lru_cache() # noqa
|
203
|
+
def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
|
204
|
+
sign = 1 if sign_str == '+' else -1
|
205
|
+
return datetime.timezone(
|
206
|
+
datetime.timedelta(
|
207
|
+
hours=sign * int(hour_str),
|
208
|
+
minutes=sign * int(minute_str),
|
209
|
+
),
|
210
|
+
)
|
211
|
+
|
212
|
+
|
213
|
+
def toml_match_to_localtime(match: re.Match) -> datetime.time:
|
214
|
+
hour_str, minute_str, sec_str, micros_str = match.groups()
|
215
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
216
|
+
return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
|
217
|
+
|
218
|
+
|
219
|
+
def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
|
220
|
+
if match.group('floatpart'):
|
221
|
+
return parse_float(match.group())
|
222
|
+
return int(match.group(), 0)
|
223
|
+
|
224
|
+
|
225
|
+
TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
|
226
|
+
|
227
|
+
# Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
|
228
|
+
# functions.
|
229
|
+
TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
|
230
|
+
TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
|
231
|
+
|
232
|
+
TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
233
|
+
TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
234
|
+
|
235
|
+
TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
236
|
+
|
237
|
+
TOML_WS = frozenset(' \t')
|
238
|
+
TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
|
239
|
+
TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
|
240
|
+
TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
|
241
|
+
TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
242
|
+
|
243
|
+
TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
|
244
|
+
{
|
245
|
+
'\\b': '\u0008', # backspace
|
246
|
+
'\\t': '\u0009', # tab
|
247
|
+
'\\n': '\u000A', # linefeed
|
248
|
+
'\\f': '\u000C', # form feed
|
249
|
+
'\\r': '\u000D', # carriage return
|
250
|
+
'\\"': '\u0022', # quote
|
251
|
+
'\\\\': '\u005C', # backslash
|
252
|
+
},
|
253
|
+
)
|
254
|
+
|
255
|
+
|
256
|
+
class TomlDecodeError(ValueError):
|
257
|
+
"""An error raised if a document is not valid TOML."""
|
258
|
+
|
259
|
+
|
260
|
+
def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
|
261
|
+
"""Parse TOML from a binary file object."""
|
262
|
+
b = fp.read()
|
263
|
+
try:
|
264
|
+
s = b.decode()
|
265
|
+
except AttributeError:
|
266
|
+
raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
|
267
|
+
return toml_loads(s, parse_float=parse_float)
|
268
|
+
|
269
|
+
|
270
|
+
def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
|
271
|
+
"""Parse TOML from a string."""
|
272
|
+
|
273
|
+
# The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
|
274
|
+
try:
|
275
|
+
src = s.replace('\r\n', '\n')
|
276
|
+
except (AttributeError, TypeError):
|
277
|
+
raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
|
278
|
+
pos = 0
|
279
|
+
out = TomlOutput(TomlNestedDict(), TomlFlags())
|
280
|
+
header: TomlKey = ()
|
281
|
+
parse_float = toml_make_safe_parse_float(parse_float)
|
282
|
+
|
283
|
+
# Parse one statement at a time (typically means one line in TOML source)
|
284
|
+
while True:
|
285
|
+
# 1. Skip line leading whitespace
|
286
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
287
|
+
|
288
|
+
# 2. Parse rules. Expect one of the following:
|
289
|
+
# - end of file
|
290
|
+
# - end of line
|
291
|
+
# - comment
|
292
|
+
# - key/value pair
|
293
|
+
# - append dict to list (and move to its namespace)
|
294
|
+
# - create dict (and move to its namespace)
|
295
|
+
# Skip trailing whitespace when applicable.
|
296
|
+
try:
|
297
|
+
char = src[pos]
|
298
|
+
except IndexError:
|
299
|
+
break
|
300
|
+
if char == '\n':
|
301
|
+
pos += 1
|
302
|
+
continue
|
303
|
+
if char in TOML_KEY_INITIAL_CHARS:
|
304
|
+
pos = toml_key_value_rule(src, pos, out, header, parse_float)
|
305
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
306
|
+
elif char == '[':
|
307
|
+
try:
|
308
|
+
second_char: ta.Optional[str] = src[pos + 1]
|
309
|
+
except IndexError:
|
310
|
+
second_char = None
|
311
|
+
out.flags.finalize_pending()
|
312
|
+
if second_char == '[':
|
313
|
+
pos, header = toml_create_list_rule(src, pos, out)
|
314
|
+
else:
|
315
|
+
pos, header = toml_create_dict_rule(src, pos, out)
|
316
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
317
|
+
elif char != '#':
|
318
|
+
raise toml_suffixed_err(src, pos, 'Invalid statement')
|
319
|
+
|
320
|
+
# 3. Skip comment
|
321
|
+
pos = toml_skip_comment(src, pos)
|
322
|
+
|
323
|
+
# 4. Expect end of line or end of file
|
324
|
+
try:
|
325
|
+
char = src[pos]
|
326
|
+
except IndexError:
|
327
|
+
break
|
328
|
+
if char != '\n':
|
329
|
+
raise toml_suffixed_err(
|
330
|
+
src, pos, 'Expected newline or end of document after a statement',
|
331
|
+
)
|
332
|
+
pos += 1
|
333
|
+
|
334
|
+
return out.data.dict
|
335
|
+
|
336
|
+
|
337
|
+
class TomlFlags:
|
338
|
+
"""Flags that map to parsed keys/namespaces."""
|
339
|
+
|
340
|
+
# Marks an immutable namespace (inline array or inline table).
|
341
|
+
FROZEN = 0
|
342
|
+
# Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
|
343
|
+
EXPLICIT_NEST = 1
|
344
|
+
|
345
|
+
def __init__(self) -> None:
|
346
|
+
self._flags: ta.Dict[str, dict] = {}
|
347
|
+
self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
|
348
|
+
|
349
|
+
def add_pending(self, key: TomlKey, flag: int) -> None:
|
350
|
+
self._pending_flags.add((key, flag))
|
351
|
+
|
352
|
+
def finalize_pending(self) -> None:
|
353
|
+
for key, flag in self._pending_flags:
|
354
|
+
self.set(key, flag, recursive=False)
|
355
|
+
self._pending_flags.clear()
|
356
|
+
|
357
|
+
def unset_all(self, key: TomlKey) -> None:
|
358
|
+
cont = self._flags
|
359
|
+
for k in key[:-1]:
|
360
|
+
if k not in cont:
|
361
|
+
return
|
362
|
+
cont = cont[k]['nested']
|
363
|
+
cont.pop(key[-1], None)
|
364
|
+
|
365
|
+
def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
|
366
|
+
cont = self._flags
|
367
|
+
key_parent, key_stem = key[:-1], key[-1]
|
368
|
+
for k in key_parent:
|
369
|
+
if k not in cont:
|
370
|
+
cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
371
|
+
cont = cont[k]['nested']
|
372
|
+
if key_stem not in cont:
|
373
|
+
cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
374
|
+
cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
|
375
|
+
|
376
|
+
def is_(self, key: TomlKey, flag: int) -> bool:
|
377
|
+
if not key:
|
378
|
+
return False # document root has no flags
|
379
|
+
cont = self._flags
|
380
|
+
for k in key[:-1]:
|
381
|
+
if k not in cont:
|
382
|
+
return False
|
383
|
+
inner_cont = cont[k]
|
384
|
+
if flag in inner_cont['recursive_flags']:
|
385
|
+
return True
|
386
|
+
cont = inner_cont['nested']
|
387
|
+
key_stem = key[-1]
|
388
|
+
if key_stem in cont:
|
389
|
+
cont = cont[key_stem]
|
390
|
+
return flag in cont['flags'] or flag in cont['recursive_flags']
|
391
|
+
return False
|
392
|
+
|
393
|
+
|
394
|
+
class TomlNestedDict:
|
395
|
+
def __init__(self) -> None:
|
396
|
+
# The parsed content of the TOML document
|
397
|
+
self.dict: ta.Dict[str, ta.Any] = {}
|
398
|
+
|
399
|
+
def get_or_create_nest(
|
400
|
+
self,
|
401
|
+
key: TomlKey,
|
402
|
+
*,
|
403
|
+
access_lists: bool = True,
|
404
|
+
) -> dict:
|
405
|
+
cont: ta.Any = self.dict
|
406
|
+
for k in key:
|
407
|
+
if k not in cont:
|
408
|
+
cont[k] = {}
|
409
|
+
cont = cont[k]
|
410
|
+
if access_lists and isinstance(cont, list):
|
411
|
+
cont = cont[-1]
|
412
|
+
if not isinstance(cont, dict):
|
413
|
+
raise KeyError('There is no nest behind this key')
|
414
|
+
return cont
|
415
|
+
|
416
|
+
def append_nest_to_list(self, key: TomlKey) -> None:
|
417
|
+
cont = self.get_or_create_nest(key[:-1])
|
418
|
+
last_key = key[-1]
|
419
|
+
if last_key in cont:
|
420
|
+
list_ = cont[last_key]
|
421
|
+
if not isinstance(list_, list):
|
422
|
+
raise KeyError('An object other than list found behind this key')
|
423
|
+
list_.append({})
|
424
|
+
else:
|
425
|
+
cont[last_key] = [{}]
|
426
|
+
|
427
|
+
|
428
|
+
class TomlOutput(ta.NamedTuple):
|
429
|
+
data: TomlNestedDict
|
430
|
+
flags: TomlFlags
|
431
|
+
|
432
|
+
|
433
|
+
def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
|
434
|
+
try:
|
435
|
+
while src[pos] in chars:
|
436
|
+
pos += 1
|
437
|
+
except IndexError:
|
438
|
+
pass
|
439
|
+
return pos
|
440
|
+
|
441
|
+
|
442
|
+
def toml_skip_until(
|
443
|
+
src: str,
|
444
|
+
pos: TomlPos,
|
445
|
+
expect: str,
|
446
|
+
*,
|
447
|
+
error_on: ta.FrozenSet[str],
|
448
|
+
error_on_eof: bool,
|
449
|
+
) -> TomlPos:
|
450
|
+
try:
|
451
|
+
new_pos = src.index(expect, pos)
|
452
|
+
except ValueError:
|
453
|
+
new_pos = len(src)
|
454
|
+
if error_on_eof:
|
455
|
+
raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
|
456
|
+
|
457
|
+
if not error_on.isdisjoint(src[pos:new_pos]):
|
458
|
+
while src[pos] not in error_on:
|
459
|
+
pos += 1
|
460
|
+
raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
|
461
|
+
return new_pos
|
462
|
+
|
463
|
+
|
464
|
+
def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
|
465
|
+
try:
|
466
|
+
char: ta.Optional[str] = src[pos]
|
467
|
+
except IndexError:
|
468
|
+
char = None
|
469
|
+
if char == '#':
|
470
|
+
return toml_skip_until(
|
471
|
+
src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
|
472
|
+
)
|
473
|
+
return pos
|
474
|
+
|
475
|
+
|
476
|
+
def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
|
477
|
+
while True:
|
478
|
+
pos_before_skip = pos
|
479
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
480
|
+
pos = toml_skip_comment(src, pos)
|
481
|
+
if pos == pos_before_skip:
|
482
|
+
return pos
|
483
|
+
|
484
|
+
|
485
|
+
def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
486
|
+
pos += 1 # Skip "["
|
487
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
488
|
+
pos, key = toml_parse_key(src, pos)
|
489
|
+
|
490
|
+
if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
|
491
|
+
raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
|
492
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
493
|
+
try:
|
494
|
+
out.data.get_or_create_nest(key)
|
495
|
+
except KeyError:
|
496
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
497
|
+
|
498
|
+
if not src.startswith(']', pos):
|
499
|
+
raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
|
500
|
+
return pos + 1, key
|
501
|
+
|
502
|
+
|
503
|
+
def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
504
|
+
pos += 2 # Skip "[["
|
505
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
506
|
+
pos, key = toml_parse_key(src, pos)
|
507
|
+
|
508
|
+
if out.flags.is_(key, TomlFlags.FROZEN):
|
509
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
510
|
+
# Free the namespace now that it points to another empty list item...
|
511
|
+
out.flags.unset_all(key)
|
512
|
+
# ...but this key precisely is still prohibited from table declaration
|
513
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
514
|
+
try:
|
515
|
+
out.data.append_nest_to_list(key)
|
516
|
+
except KeyError:
|
517
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
518
|
+
|
519
|
+
if not src.startswith(']]', pos):
|
520
|
+
raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
|
521
|
+
return pos + 2, key
|
522
|
+
|
523
|
+
|
524
|
+
def toml_key_value_rule(
|
525
|
+
src: str,
|
526
|
+
pos: TomlPos,
|
527
|
+
out: TomlOutput,
|
528
|
+
header: TomlKey,
|
529
|
+
parse_float: TomlParseFloat,
|
530
|
+
) -> TomlPos:
|
531
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
532
|
+
key_parent, key_stem = key[:-1], key[-1]
|
533
|
+
abs_key_parent = header + key_parent
|
534
|
+
|
535
|
+
relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
|
536
|
+
for cont_key in relative_path_cont_keys:
|
537
|
+
# Check that dotted key syntax does not redefine an existing table
|
538
|
+
if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
|
539
|
+
raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
|
540
|
+
# Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
|
541
|
+
# table sections.
|
542
|
+
out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
|
543
|
+
|
544
|
+
if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
|
545
|
+
raise toml_suffixed_err(
|
546
|
+
src,
|
547
|
+
pos,
|
548
|
+
f'Cannot mutate immutable namespace {abs_key_parent}',
|
549
|
+
)
|
550
|
+
|
551
|
+
try:
|
552
|
+
nest = out.data.get_or_create_nest(abs_key_parent)
|
553
|
+
except KeyError:
|
554
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
555
|
+
if key_stem in nest:
|
556
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
|
557
|
+
# Mark inline table and array namespaces recursively immutable
|
558
|
+
if isinstance(value, (dict, list)):
|
559
|
+
out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
|
560
|
+
nest[key_stem] = value
|
561
|
+
return pos
|
562
|
+
|
563
|
+
|
564
|
+
def toml_parse_key_value_pair(
|
565
|
+
src: str,
|
566
|
+
pos: TomlPos,
|
567
|
+
parse_float: TomlParseFloat,
|
568
|
+
) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
|
569
|
+
pos, key = toml_parse_key(src, pos)
|
570
|
+
try:
|
571
|
+
char: ta.Optional[str] = src[pos]
|
572
|
+
except IndexError:
|
573
|
+
char = None
|
574
|
+
if char != '=':
|
575
|
+
raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
|
576
|
+
pos += 1
|
577
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
578
|
+
pos, value = toml_parse_value(src, pos, parse_float)
|
579
|
+
return pos, key, value
|
580
|
+
|
581
|
+
|
582
|
+
def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
|
583
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
584
|
+
key: TomlKey = (key_part,)
|
585
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
586
|
+
while True:
|
587
|
+
try:
|
588
|
+
char: ta.Optional[str] = src[pos]
|
589
|
+
except IndexError:
|
590
|
+
char = None
|
591
|
+
if char != '.':
|
592
|
+
return pos, key
|
593
|
+
pos += 1
|
594
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
595
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
596
|
+
key += (key_part,)
|
597
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
598
|
+
|
599
|
+
|
600
|
+
def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
601
|
+
try:
|
602
|
+
char: ta.Optional[str] = src[pos]
|
603
|
+
except IndexError:
|
604
|
+
char = None
|
605
|
+
if char in TOML_BARE_KEY_CHARS:
|
606
|
+
start_pos = pos
|
607
|
+
pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
|
608
|
+
return pos, src[start_pos:pos]
|
609
|
+
if char == "'":
|
610
|
+
return toml_parse_literal_str(src, pos)
|
611
|
+
if char == '"':
|
612
|
+
return toml_parse_one_line_basic_str(src, pos)
|
613
|
+
raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
|
614
|
+
|
615
|
+
|
616
|
+
def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
617
|
+
pos += 1
|
618
|
+
return toml_parse_basic_str(src, pos, multiline=False)
|
619
|
+
|
620
|
+
|
621
|
+
def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
|
622
|
+
pos += 1
|
623
|
+
array: list = []
|
624
|
+
|
625
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
626
|
+
if src.startswith(']', pos):
|
627
|
+
return pos + 1, array
|
628
|
+
while True:
|
629
|
+
pos, val = toml_parse_value(src, pos, parse_float)
|
630
|
+
array.append(val)
|
631
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
632
|
+
|
633
|
+
c = src[pos:pos + 1]
|
634
|
+
if c == ']':
|
635
|
+
return pos + 1, array
|
636
|
+
if c != ',':
|
637
|
+
raise toml_suffixed_err(src, pos, 'Unclosed array')
|
638
|
+
pos += 1
|
639
|
+
|
640
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
641
|
+
if src.startswith(']', pos):
|
642
|
+
return pos + 1, array
|
643
|
+
|
644
|
+
|
645
|
+
def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
|
646
|
+
pos += 1
|
647
|
+
nested_dict = TomlNestedDict()
|
648
|
+
flags = TomlFlags()
|
649
|
+
|
650
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
651
|
+
if src.startswith('}', pos):
|
652
|
+
return pos + 1, nested_dict.dict
|
653
|
+
while True:
|
654
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
655
|
+
key_parent, key_stem = key[:-1], key[-1]
|
656
|
+
if flags.is_(key, TomlFlags.FROZEN):
|
657
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
658
|
+
try:
|
659
|
+
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
660
|
+
except KeyError:
|
661
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
662
|
+
if key_stem in nest:
|
663
|
+
raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
|
664
|
+
nest[key_stem] = value
|
665
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
666
|
+
c = src[pos:pos + 1]
|
667
|
+
if c == '}':
|
668
|
+
return pos + 1, nested_dict.dict
|
669
|
+
if c != ',':
|
670
|
+
raise toml_suffixed_err(src, pos, 'Unclosed inline table')
|
671
|
+
if isinstance(value, (dict, list)):
|
672
|
+
flags.set(key, TomlFlags.FROZEN, recursive=True)
|
673
|
+
pos += 1
|
674
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
675
|
+
|
676
|
+
|
677
|
+
def toml_parse_basic_str_escape(
|
678
|
+
src: str,
|
679
|
+
pos: TomlPos,
|
680
|
+
*,
|
681
|
+
multiline: bool = False,
|
682
|
+
) -> ta.Tuple[TomlPos, str]:
|
683
|
+
escape_id = src[pos:pos + 2]
|
684
|
+
pos += 2
|
685
|
+
if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
|
686
|
+
# Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
|
687
|
+
# newline.
|
688
|
+
if escape_id != '\\\n':
|
689
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
690
|
+
try:
|
691
|
+
char = src[pos]
|
692
|
+
except IndexError:
|
693
|
+
return pos, ''
|
694
|
+
if char != '\n':
|
695
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
|
696
|
+
pos += 1
|
697
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
698
|
+
return pos, ''
|
699
|
+
if escape_id == '\\u':
|
700
|
+
return toml_parse_hex_char(src, pos, 4)
|
701
|
+
if escape_id == '\\U':
|
702
|
+
return toml_parse_hex_char(src, pos, 8)
|
703
|
+
try:
|
704
|
+
return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
705
|
+
except KeyError:
|
706
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
707
|
+
|
708
|
+
|
709
|
+
def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
710
|
+
return toml_parse_basic_str_escape(src, pos, multiline=True)
|
711
|
+
|
712
|
+
|
713
|
+
def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
|
714
|
+
hex_str = src[pos:pos + hex_len]
|
715
|
+
if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
|
716
|
+
raise toml_suffixed_err(src, pos, 'Invalid hex value')
|
717
|
+
pos += hex_len
|
718
|
+
hex_int = int(hex_str, 16)
|
719
|
+
if not toml_is_unicode_scalar_value(hex_int):
|
720
|
+
raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
|
721
|
+
return pos, chr(hex_int)
|
722
|
+
|
723
|
+
|
724
|
+
def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
725
|
+
pos += 1 # Skip starting apostrophe
|
726
|
+
start_pos = pos
|
727
|
+
pos = toml_skip_until(
|
728
|
+
src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
|
729
|
+
)
|
730
|
+
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
|
731
|
+
|
732
|
+
|
733
|
+
def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
|
734
|
+
pos += 3
|
735
|
+
if src.startswith('\n', pos):
|
736
|
+
pos += 1
|
737
|
+
|
738
|
+
if literal:
|
739
|
+
delim = "'"
|
740
|
+
end_pos = toml_skip_until(
|
741
|
+
src,
|
742
|
+
pos,
|
743
|
+
"'''",
|
744
|
+
error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
745
|
+
error_on_eof=True,
|
746
|
+
)
|
747
|
+
result = src[pos:end_pos]
|
748
|
+
pos = end_pos + 3
|
749
|
+
else:
|
750
|
+
delim = '"'
|
751
|
+
pos, result = toml_parse_basic_str(src, pos, multiline=True)
|
752
|
+
|
753
|
+
# Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
|
754
|
+
if not src.startswith(delim, pos):
|
755
|
+
return pos, result
|
756
|
+
pos += 1
|
757
|
+
if not src.startswith(delim, pos):
|
758
|
+
return pos, result + delim
|
759
|
+
pos += 1
|
760
|
+
return pos, result + (delim * 2)
|
761
|
+
|
762
|
+
|
763
|
+
def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
|
764
|
+
if multiline:
|
765
|
+
error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
766
|
+
parse_escapes = toml_parse_basic_str_escape_multiline
|
767
|
+
else:
|
768
|
+
error_on = TOML_ILLEGAL_BASIC_STR_CHARS
|
769
|
+
parse_escapes = toml_parse_basic_str_escape
|
770
|
+
result = ''
|
771
|
+
start_pos = pos
|
772
|
+
while True:
|
773
|
+
try:
|
774
|
+
char = src[pos]
|
775
|
+
except IndexError:
|
776
|
+
raise toml_suffixed_err(src, pos, 'Unterminated string') from None
|
777
|
+
if char == '"':
|
778
|
+
if not multiline:
|
779
|
+
return pos + 1, result + src[start_pos:pos]
|
780
|
+
if src.startswith('"""', pos):
|
781
|
+
return pos + 3, result + src[start_pos:pos]
|
782
|
+
pos += 1
|
783
|
+
continue
|
784
|
+
if char == '\\':
|
785
|
+
result += src[start_pos:pos]
|
786
|
+
pos, parsed_escape = parse_escapes(src, pos)
|
787
|
+
result += parsed_escape
|
788
|
+
start_pos = pos
|
789
|
+
continue
|
790
|
+
if char in error_on:
|
791
|
+
raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
|
792
|
+
pos += 1
|
793
|
+
|
794
|
+
|
795
|
+
def toml_parse_value( # noqa: C901
|
796
|
+
src: str,
|
797
|
+
pos: TomlPos,
|
798
|
+
parse_float: TomlParseFloat,
|
799
|
+
) -> ta.Tuple[TomlPos, ta.Any]:
|
800
|
+
try:
|
801
|
+
char: ta.Optional[str] = src[pos]
|
802
|
+
except IndexError:
|
803
|
+
char = None
|
804
|
+
|
805
|
+
# IMPORTANT: order conditions based on speed of checking and likelihood
|
806
|
+
|
807
|
+
# Basic strings
|
808
|
+
if char == '"':
|
809
|
+
if src.startswith('"""', pos):
|
810
|
+
return toml_parse_multiline_str(src, pos, literal=False)
|
811
|
+
return toml_parse_one_line_basic_str(src, pos)
|
812
|
+
|
813
|
+
# Literal strings
|
814
|
+
if char == "'":
|
815
|
+
if src.startswith("'''", pos):
|
816
|
+
return toml_parse_multiline_str(src, pos, literal=True)
|
817
|
+
return toml_parse_literal_str(src, pos)
|
818
|
+
|
819
|
+
# Booleans
|
820
|
+
if char == 't':
|
821
|
+
if src.startswith('true', pos):
|
822
|
+
return pos + 4, True
|
823
|
+
if char == 'f':
|
824
|
+
if src.startswith('false', pos):
|
825
|
+
return pos + 5, False
|
826
|
+
|
827
|
+
# Arrays
|
828
|
+
if char == '[':
|
829
|
+
return toml_parse_array(src, pos, parse_float)
|
830
|
+
|
831
|
+
# Inline tables
|
832
|
+
if char == '{':
|
833
|
+
return toml_parse_inline_table(src, pos, parse_float)
|
834
|
+
|
835
|
+
# Dates and times
|
836
|
+
datetime_match = TOML_RE_DATETIME.match(src, pos)
|
837
|
+
if datetime_match:
|
838
|
+
try:
|
839
|
+
datetime_obj = toml_match_to_datetime(datetime_match)
|
840
|
+
except ValueError as e:
|
841
|
+
raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
|
842
|
+
return datetime_match.end(), datetime_obj
|
843
|
+
localtime_match = TOML_RE_LOCALTIME.match(src, pos)
|
844
|
+
if localtime_match:
|
845
|
+
return localtime_match.end(), toml_match_to_localtime(localtime_match)
|
846
|
+
|
847
|
+
# Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
|
848
|
+
# located after handling of dates and times.
|
849
|
+
number_match = TOML_RE_NUMBER.match(src, pos)
|
850
|
+
if number_match:
|
851
|
+
return number_match.end(), toml_match_to_number(number_match, parse_float)
|
852
|
+
|
853
|
+
# Special floats
|
854
|
+
first_three = src[pos:pos + 3]
|
855
|
+
if first_three in {'inf', 'nan'}:
|
856
|
+
return pos + 3, parse_float(first_three)
|
857
|
+
first_four = src[pos:pos + 4]
|
858
|
+
if first_four in {'-inf', '+inf', '-nan', '+nan'}:
|
859
|
+
return pos + 4, parse_float(first_four)
|
860
|
+
|
861
|
+
raise toml_suffixed_err(src, pos, 'Invalid value')
|
862
|
+
|
863
|
+
|
864
|
+
def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
|
865
|
+
"""Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
|
866
|
+
|
867
|
+
def coord_repr(src: str, pos: TomlPos) -> str:
|
868
|
+
if pos >= len(src):
|
869
|
+
return 'end of document'
|
870
|
+
line = src.count('\n', 0, pos) + 1
|
871
|
+
if line == 1:
|
872
|
+
column = pos + 1
|
873
|
+
else:
|
874
|
+
column = pos - src.rindex('\n', 0, pos)
|
875
|
+
return f'line {line}, column {column}'
|
876
|
+
|
877
|
+
return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
|
878
|
+
|
879
|
+
|
880
|
+
def toml_is_unicode_scalar_value(codepoint: int) -> bool:
|
881
|
+
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
|
882
|
+
|
883
|
+
|
884
|
+
def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
|
885
|
+
"""A decorator to make `parse_float` safe.
|
886
|
+
|
887
|
+
`parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
|
888
|
+
thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
|
889
|
+
"""
|
890
|
+
# The default `float` callable never returns illegal types. Optimize it.
|
891
|
+
if parse_float is float:
|
892
|
+
return float
|
893
|
+
|
894
|
+
def safe_parse_float(float_str: str) -> ta.Any:
|
895
|
+
float_value = parse_float(float_str)
|
896
|
+
if isinstance(float_value, (dict, list)):
|
897
|
+
raise ValueError('parse_float must not return dicts or lists') # noqa
|
898
|
+
return float_value
|
899
|
+
|
900
|
+
return safe_parse_float
|
901
|
+
|
63
902
|
|
64
903
|
########################################
|
65
904
|
# ../compat.py
|
@@ -215,6 +1054,19 @@ def close_fd(fd: int) -> bool:
|
|
215
1054
|
return True
|
216
1055
|
|
217
1056
|
|
1057
|
+
def is_fd_open(fd: int) -> bool:
|
1058
|
+
try:
|
1059
|
+
n = os.dup(fd)
|
1060
|
+
except OSError:
|
1061
|
+
return False
|
1062
|
+
os.close(n)
|
1063
|
+
return True
|
1064
|
+
|
1065
|
+
|
1066
|
+
def get_open_fds(limit: int) -> ta.FrozenSet[int]:
|
1067
|
+
return frozenset(filter(is_fd_open, range(limit)))
|
1068
|
+
|
1069
|
+
|
218
1070
|
def mktempfile(suffix: str, prefix: str, dir: str) -> str: # noqa
|
219
1071
|
fd, filename = tempfile.mkstemp(suffix, prefix, dir)
|
220
1072
|
os.close(fd)
|
@@ -489,7 +1341,7 @@ class _cached_nullary: # noqa
|
|
489
1341
|
return bound
|
490
1342
|
|
491
1343
|
|
492
|
-
def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
|
1344
|
+
def cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
|
493
1345
|
return _cached_nullary(fn)
|
494
1346
|
|
495
1347
|
|
@@ -580,6 +1432,50 @@ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON
|
|
580
1432
|
json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
|
581
1433
|
|
582
1434
|
|
1435
|
+
########################################
|
1436
|
+
# ../../../omlish/lite/maybes.py
|
1437
|
+
|
1438
|
+
|
1439
|
+
class Maybe(ta.Generic[T]):
|
1440
|
+
@property
|
1441
|
+
@abc.abstractmethod
|
1442
|
+
def present(self) -> bool:
|
1443
|
+
raise NotImplementedError
|
1444
|
+
|
1445
|
+
@abc.abstractmethod
|
1446
|
+
def must(self) -> T:
|
1447
|
+
raise NotImplementedError
|
1448
|
+
|
1449
|
+
@classmethod
|
1450
|
+
def just(cls, v: T) -> 'Maybe[T]':
|
1451
|
+
return tuple.__new__(_Maybe, (v,)) # noqa
|
1452
|
+
|
1453
|
+
_empty: ta.ClassVar['Maybe']
|
1454
|
+
|
1455
|
+
@classmethod
|
1456
|
+
def empty(cls) -> 'Maybe[T]':
|
1457
|
+
return Maybe._empty
|
1458
|
+
|
1459
|
+
|
1460
|
+
class _Maybe(Maybe[T], tuple):
|
1461
|
+
__slots__ = ()
|
1462
|
+
|
1463
|
+
def __init_subclass__(cls, **kwargs):
|
1464
|
+
raise TypeError
|
1465
|
+
|
1466
|
+
@property
|
1467
|
+
def present(self) -> bool:
|
1468
|
+
return bool(self)
|
1469
|
+
|
1470
|
+
def must(self) -> T:
|
1471
|
+
if not self:
|
1472
|
+
raise ValueError
|
1473
|
+
return self[0]
|
1474
|
+
|
1475
|
+
|
1476
|
+
Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
|
1477
|
+
|
1478
|
+
|
583
1479
|
########################################
|
584
1480
|
# ../../../omlish/lite/reflect.py
|
585
1481
|
|
@@ -628,179 +1524,681 @@ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
|
|
628
1524
|
|
629
1525
|
|
630
1526
|
########################################
|
631
|
-
# ../
|
1527
|
+
# ../states.py
|
1528
|
+
|
1529
|
+
|
1530
|
+
##
|
1531
|
+
|
1532
|
+
|
1533
|
+
def _names_by_code(states: ta.Any) -> ta.Dict[int, str]:
|
1534
|
+
d = {}
|
1535
|
+
for name in states.__dict__:
|
1536
|
+
if not name.startswith('__'):
|
1537
|
+
code = getattr(states, name)
|
1538
|
+
d[code] = name
|
1539
|
+
return d
|
1540
|
+
|
1541
|
+
|
1542
|
+
##
|
1543
|
+
|
1544
|
+
|
1545
|
+
class ProcessStates:
|
1546
|
+
STOPPED = 0
|
1547
|
+
STARTING = 10
|
1548
|
+
RUNNING = 20
|
1549
|
+
BACKOFF = 30
|
1550
|
+
STOPPING = 40
|
1551
|
+
EXITED = 100
|
1552
|
+
FATAL = 200
|
1553
|
+
UNKNOWN = 1000
|
1554
|
+
|
1555
|
+
|
1556
|
+
STOPPED_STATES = (
|
1557
|
+
ProcessStates.STOPPED,
|
1558
|
+
ProcessStates.EXITED,
|
1559
|
+
ProcessStates.FATAL,
|
1560
|
+
ProcessStates.UNKNOWN,
|
1561
|
+
)
|
1562
|
+
|
1563
|
+
RUNNING_STATES = (
|
1564
|
+
ProcessStates.RUNNING,
|
1565
|
+
ProcessStates.BACKOFF,
|
1566
|
+
ProcessStates.STARTING,
|
1567
|
+
)
|
1568
|
+
|
1569
|
+
SIGNALLABLE_STATES = (
|
1570
|
+
ProcessStates.RUNNING,
|
1571
|
+
ProcessStates.STARTING,
|
1572
|
+
ProcessStates.STOPPING,
|
1573
|
+
)
|
1574
|
+
|
1575
|
+
|
1576
|
+
_process_states_by_code = _names_by_code(ProcessStates)
|
1577
|
+
|
1578
|
+
|
1579
|
+
def get_process_state_description(code: ProcessState) -> str:
|
1580
|
+
return check_not_none(_process_states_by_code.get(code))
|
1581
|
+
|
1582
|
+
|
1583
|
+
##
|
1584
|
+
|
1585
|
+
|
1586
|
+
class SupervisorStates:
|
1587
|
+
FATAL = 2
|
1588
|
+
RUNNING = 1
|
1589
|
+
RESTARTING = 0
|
1590
|
+
SHUTDOWN = -1
|
1591
|
+
|
1592
|
+
|
1593
|
+
_supervisor_states_by_code = _names_by_code(SupervisorStates)
|
1594
|
+
|
1595
|
+
|
1596
|
+
def get_supervisor_state_description(code: SupervisorState) -> str:
|
1597
|
+
return check_not_none(_supervisor_states_by_code.get(code))
|
1598
|
+
|
1599
|
+
|
1600
|
+
########################################
|
1601
|
+
# ../../../omlish/lite/inject.py
|
1602
|
+
|
1603
|
+
|
1604
|
+
###
|
1605
|
+
# types
|
632
1606
|
|
633
1607
|
|
634
1608
|
@dc.dataclass(frozen=True)
|
635
|
-
class
|
636
|
-
|
637
|
-
|
1609
|
+
class InjectorKey:
|
1610
|
+
cls: InjectorKeyCls
|
1611
|
+
tag: ta.Any = None
|
1612
|
+
array: bool = False
|
638
1613
|
|
639
|
-
uid: ta.Optional[int] = None
|
640
|
-
directory: ta.Optional[str] = None
|
641
|
-
umask: ta.Optional[int] = None
|
642
|
-
priority: int = 999
|
643
1614
|
|
644
|
-
|
645
|
-
autorestart: str = 'unexpected'
|
1615
|
+
##
|
646
1616
|
|
647
|
-
startsecs: int = 1
|
648
|
-
startretries: int = 3
|
649
1617
|
|
650
|
-
|
651
|
-
|
1618
|
+
class InjectorProvider(abc.ABC):
|
1619
|
+
@abc.abstractmethod
|
1620
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1621
|
+
raise NotImplementedError
|
652
1622
|
|
653
|
-
@dc.dataclass(frozen=True)
|
654
|
-
class Log:
|
655
|
-
file: ta.Optional[str] = None
|
656
|
-
capture_maxbytes: ta.Optional[int] = None
|
657
|
-
events_enabled: bool = False
|
658
|
-
syslog: bool = False
|
659
|
-
backups: ta.Optional[int] = None
|
660
|
-
maxbytes: ta.Optional[int] = None
|
661
1623
|
|
662
|
-
|
663
|
-
stderr: Log = Log()
|
1624
|
+
##
|
664
1625
|
|
665
|
-
stopsignal: int = signal.SIGTERM
|
666
|
-
stopwaitsecs: int = 10
|
667
|
-
stopasgroup: bool = False
|
668
1626
|
|
669
|
-
|
1627
|
+
@dc.dataclass(frozen=True)
|
1628
|
+
class InjectorBinding:
|
1629
|
+
key: InjectorKey
|
1630
|
+
provider: InjectorProvider
|
670
1631
|
|
671
|
-
exitcodes: ta.Sequence[int] = (0,)
|
672
1632
|
|
673
|
-
|
1633
|
+
class InjectorBindings(abc.ABC):
|
1634
|
+
@abc.abstractmethod
|
1635
|
+
def bindings(self) -> ta.Iterator[InjectorBinding]:
|
1636
|
+
raise NotImplementedError
|
674
1637
|
|
675
|
-
|
1638
|
+
##
|
1639
|
+
|
1640
|
+
|
1641
|
+
class Injector(abc.ABC):
|
1642
|
+
@abc.abstractmethod
|
1643
|
+
def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
|
1644
|
+
raise NotImplementedError
|
1645
|
+
|
1646
|
+
@abc.abstractmethod
|
1647
|
+
def provide(self, key: ta.Any) -> ta.Any:
|
1648
|
+
raise NotImplementedError
|
1649
|
+
|
1650
|
+
@abc.abstractmethod
|
1651
|
+
def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
|
1652
|
+
raise NotImplementedError
|
1653
|
+
|
1654
|
+
@abc.abstractmethod
|
1655
|
+
def inject(self, obj: ta.Any) -> ta.Any:
|
1656
|
+
raise NotImplementedError
|
1657
|
+
|
1658
|
+
|
1659
|
+
###
|
1660
|
+
# exceptions
|
676
1661
|
|
677
1662
|
|
678
1663
|
@dc.dataclass(frozen=True)
|
679
|
-
class
|
1664
|
+
class InjectorKeyError(Exception):
|
1665
|
+
key: InjectorKey
|
1666
|
+
|
1667
|
+
source: ta.Any = None
|
1668
|
+
name: ta.Optional[str] = None
|
1669
|
+
|
1670
|
+
|
1671
|
+
@dc.dataclass(frozen=True)
|
1672
|
+
class UnboundInjectorKeyError(InjectorKeyError):
|
1673
|
+
pass
|
1674
|
+
|
1675
|
+
|
1676
|
+
@dc.dataclass(frozen=True)
|
1677
|
+
class DuplicateInjectorKeyError(InjectorKeyError):
|
1678
|
+
pass
|
1679
|
+
|
1680
|
+
|
1681
|
+
###
|
1682
|
+
# keys
|
1683
|
+
|
1684
|
+
|
1685
|
+
def as_injector_key(o: ta.Any) -> InjectorKey:
|
1686
|
+
if o is inspect.Parameter.empty:
|
1687
|
+
raise TypeError(o)
|
1688
|
+
if isinstance(o, InjectorKey):
|
1689
|
+
return o
|
1690
|
+
if isinstance(o, (type, ta.NewType)):
|
1691
|
+
return InjectorKey(o)
|
1692
|
+
raise TypeError(o)
|
1693
|
+
|
1694
|
+
|
1695
|
+
###
|
1696
|
+
# providers
|
1697
|
+
|
1698
|
+
|
1699
|
+
@dc.dataclass(frozen=True)
|
1700
|
+
class FnInjectorProvider(InjectorProvider):
|
1701
|
+
fn: ta.Any
|
1702
|
+
|
1703
|
+
def __post_init__(self) -> None:
|
1704
|
+
check_not_isinstance(self.fn, type)
|
1705
|
+
|
1706
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1707
|
+
def pfn(i: Injector) -> ta.Any:
|
1708
|
+
return i.inject(self.fn)
|
1709
|
+
|
1710
|
+
return pfn
|
1711
|
+
|
1712
|
+
|
1713
|
+
@dc.dataclass(frozen=True)
|
1714
|
+
class CtorInjectorProvider(InjectorProvider):
|
1715
|
+
cls: type
|
1716
|
+
|
1717
|
+
def __post_init__(self) -> None:
|
1718
|
+
check_isinstance(self.cls, type)
|
1719
|
+
|
1720
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1721
|
+
def pfn(i: Injector) -> ta.Any:
|
1722
|
+
return i.inject(self.cls)
|
1723
|
+
|
1724
|
+
return pfn
|
1725
|
+
|
1726
|
+
|
1727
|
+
@dc.dataclass(frozen=True)
|
1728
|
+
class ConstInjectorProvider(InjectorProvider):
|
1729
|
+
v: ta.Any
|
1730
|
+
|
1731
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1732
|
+
return lambda _: self.v
|
1733
|
+
|
1734
|
+
|
1735
|
+
@dc.dataclass(frozen=True)
|
1736
|
+
class SingletonInjectorProvider(InjectorProvider):
|
1737
|
+
p: InjectorProvider
|
1738
|
+
|
1739
|
+
def __post_init__(self) -> None:
|
1740
|
+
check_isinstance(self.p, InjectorProvider)
|
1741
|
+
|
1742
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1743
|
+
v = not_set = object()
|
1744
|
+
|
1745
|
+
def pfn(i: Injector) -> ta.Any:
|
1746
|
+
nonlocal v
|
1747
|
+
if v is not_set:
|
1748
|
+
v = ufn(i)
|
1749
|
+
return v
|
1750
|
+
|
1751
|
+
ufn = self.p.provider_fn()
|
1752
|
+
return pfn
|
1753
|
+
|
1754
|
+
|
1755
|
+
@dc.dataclass(frozen=True)
|
1756
|
+
class LinkInjectorProvider(InjectorProvider):
|
1757
|
+
k: InjectorKey
|
1758
|
+
|
1759
|
+
def __post_init__(self) -> None:
|
1760
|
+
check_isinstance(self.k, InjectorKey)
|
1761
|
+
|
1762
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1763
|
+
def pfn(i: Injector) -> ta.Any:
|
1764
|
+
return i.provide(self.k)
|
1765
|
+
|
1766
|
+
return pfn
|
1767
|
+
|
1768
|
+
|
1769
|
+
@dc.dataclass(frozen=True)
|
1770
|
+
class ArrayInjectorProvider(InjectorProvider):
|
1771
|
+
ps: ta.Sequence[InjectorProvider]
|
1772
|
+
|
1773
|
+
def provider_fn(self) -> InjectorProviderFn:
|
1774
|
+
ps = [p.provider_fn() for p in self.ps]
|
1775
|
+
|
1776
|
+
def pfn(i: Injector) -> ta.Any:
|
1777
|
+
rv = []
|
1778
|
+
for ep in ps:
|
1779
|
+
o = ep(i)
|
1780
|
+
rv.append(o)
|
1781
|
+
return rv
|
1782
|
+
|
1783
|
+
return pfn
|
1784
|
+
|
1785
|
+
|
1786
|
+
###
|
1787
|
+
# bindings
|
1788
|
+
|
1789
|
+
|
1790
|
+
@dc.dataclass(frozen=True)
|
1791
|
+
class _InjectorBindings(InjectorBindings):
|
1792
|
+
bs: ta.Optional[ta.Sequence[InjectorBinding]] = None
|
1793
|
+
ps: ta.Optional[ta.Sequence[InjectorBindings]] = None
|
1794
|
+
|
1795
|
+
def bindings(self) -> ta.Iterator[InjectorBinding]:
|
1796
|
+
if self.bs is not None:
|
1797
|
+
yield from self.bs
|
1798
|
+
if self.ps is not None:
|
1799
|
+
for p in self.ps:
|
1800
|
+
yield from p.bindings()
|
1801
|
+
|
1802
|
+
|
1803
|
+
def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
|
1804
|
+
bs: ta.List[InjectorBinding] = []
|
1805
|
+
ps: ta.List[InjectorBindings] = []
|
1806
|
+
for a in args:
|
1807
|
+
if isinstance(a, InjectorBindings):
|
1808
|
+
ps.append(a)
|
1809
|
+
elif isinstance(a, InjectorBinding):
|
1810
|
+
bs.append(a)
|
1811
|
+
else:
|
1812
|
+
raise TypeError(a)
|
1813
|
+
return _InjectorBindings(
|
1814
|
+
bs or None,
|
1815
|
+
ps or None,
|
1816
|
+
)
|
1817
|
+
|
1818
|
+
|
1819
|
+
##
|
1820
|
+
|
1821
|
+
|
1822
|
+
@dc.dataclass(frozen=True)
|
1823
|
+
class OverridesInjectorBindings(InjectorBindings):
|
1824
|
+
p: InjectorBindings
|
1825
|
+
m: ta.Mapping[InjectorKey, InjectorBinding]
|
1826
|
+
|
1827
|
+
def bindings(self) -> ta.Iterator[InjectorBinding]:
|
1828
|
+
for b in self.p.bindings():
|
1829
|
+
yield self.m.get(b.key, b)
|
1830
|
+
|
1831
|
+
|
1832
|
+
def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
1833
|
+
m: ta.Dict[InjectorKey, InjectorBinding] = {}
|
1834
|
+
for b in as_injector_bindings(*args).bindings():
|
1835
|
+
if b.key in m:
|
1836
|
+
raise DuplicateInjectorKeyError(b.key)
|
1837
|
+
m[b.key] = b
|
1838
|
+
return OverridesInjectorBindings(p, m)
|
1839
|
+
|
1840
|
+
|
1841
|
+
##
|
1842
|
+
|
1843
|
+
|
1844
|
+
def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
|
1845
|
+
pm: ta.Dict[InjectorKey, InjectorProvider] = {}
|
1846
|
+
am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
|
1847
|
+
|
1848
|
+
for b in bs.bindings():
|
1849
|
+
if b.key.array:
|
1850
|
+
am.setdefault(b.key, []).append(b.provider)
|
1851
|
+
else:
|
1852
|
+
if b.key in pm:
|
1853
|
+
raise KeyError(b.key)
|
1854
|
+
pm[b.key] = b.provider
|
1855
|
+
|
1856
|
+
if am:
|
1857
|
+
for k, aps in am.items():
|
1858
|
+
pm[k] = ArrayInjectorProvider(aps)
|
1859
|
+
|
1860
|
+
return pm
|
1861
|
+
|
1862
|
+
|
1863
|
+
###
|
1864
|
+
# inspection
|
1865
|
+
|
1866
|
+
|
1867
|
+
_INJECTION_SIGNATURE_CACHE: ta.MutableMapping[ta.Any, inspect.Signature] = weakref.WeakKeyDictionary()
|
1868
|
+
|
1869
|
+
|
1870
|
+
def _injection_signature(obj: ta.Any) -> inspect.Signature:
|
1871
|
+
try:
|
1872
|
+
return _INJECTION_SIGNATURE_CACHE[obj]
|
1873
|
+
except TypeError:
|
1874
|
+
return inspect.signature(obj)
|
1875
|
+
except KeyError:
|
1876
|
+
pass
|
1877
|
+
sig = inspect.signature(obj)
|
1878
|
+
_INJECTION_SIGNATURE_CACHE[obj] = sig
|
1879
|
+
return sig
|
1880
|
+
|
1881
|
+
|
1882
|
+
class InjectionKwarg(ta.NamedTuple):
|
680
1883
|
name: str
|
1884
|
+
key: InjectorKey
|
1885
|
+
has_default: bool
|
1886
|
+
|
1887
|
+
|
1888
|
+
class InjectionKwargsTarget(ta.NamedTuple):
|
1889
|
+
obj: ta.Any
|
1890
|
+
kwargs: ta.Sequence[InjectionKwarg]
|
1891
|
+
|
1892
|
+
|
1893
|
+
def build_injection_kwargs_target(
|
1894
|
+
obj: ta.Any,
|
1895
|
+
*,
|
1896
|
+
skip_args: int = 0,
|
1897
|
+
skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
|
1898
|
+
raw_optional: bool = False,
|
1899
|
+
) -> InjectionKwargsTarget:
|
1900
|
+
sig = _injection_signature(obj)
|
1901
|
+
|
1902
|
+
seen: ta.Set[InjectorKey] = set(map(as_injector_key, skip_kwargs)) if skip_kwargs is not None else set()
|
1903
|
+
kws: ta.List[InjectionKwarg] = []
|
1904
|
+
for p in list(sig.parameters.values())[skip_args:]:
|
1905
|
+
if p.annotation is inspect.Signature.empty:
|
1906
|
+
if p.default is not inspect.Parameter.empty:
|
1907
|
+
raise KeyError(f'{obj}, {p.name}')
|
1908
|
+
continue
|
1909
|
+
|
1910
|
+
if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
|
1911
|
+
raise TypeError(sig)
|
1912
|
+
|
1913
|
+
ann = p.annotation
|
1914
|
+
if (
|
1915
|
+
not raw_optional and
|
1916
|
+
is_optional_alias(ann)
|
1917
|
+
):
|
1918
|
+
ann = get_optional_alias_arg(ann)
|
1919
|
+
|
1920
|
+
k = as_injector_key(ann)
|
1921
|
+
|
1922
|
+
if k in seen:
|
1923
|
+
raise DuplicateInjectorKeyError(k)
|
1924
|
+
seen.add(k)
|
1925
|
+
|
1926
|
+
kws.append(InjectionKwarg(
|
1927
|
+
p.name,
|
1928
|
+
k,
|
1929
|
+
p.default is not inspect.Parameter.empty,
|
1930
|
+
))
|
1931
|
+
|
1932
|
+
return InjectionKwargsTarget(
|
1933
|
+
obj,
|
1934
|
+
kws,
|
1935
|
+
)
|
1936
|
+
|
1937
|
+
|
1938
|
+
###
|
1939
|
+
# binder
|
1940
|
+
|
1941
|
+
|
1942
|
+
class InjectorBinder:
|
1943
|
+
def __new__(cls, *args, **kwargs): # noqa
|
1944
|
+
raise TypeError
|
1945
|
+
|
1946
|
+
_FN_TYPES: ta.Tuple[type, ...] = (
|
1947
|
+
types.FunctionType,
|
1948
|
+
types.MethodType,
|
1949
|
+
|
1950
|
+
classmethod,
|
1951
|
+
staticmethod,
|
1952
|
+
|
1953
|
+
functools.partial,
|
1954
|
+
functools.partialmethod,
|
1955
|
+
)
|
1956
|
+
|
1957
|
+
@classmethod
|
1958
|
+
def _is_fn(cls, obj: ta.Any) -> bool:
|
1959
|
+
return isinstance(obj, cls._FN_TYPES)
|
1960
|
+
|
1961
|
+
@classmethod
|
1962
|
+
def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
|
1963
|
+
check_isinstance(icls, type)
|
1964
|
+
if icls not in cls._FN_TYPES:
|
1965
|
+
cls._FN_TYPES = (*cls._FN_TYPES, icls)
|
1966
|
+
return icls
|
1967
|
+
|
1968
|
+
_BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
|
1969
|
+
InjectorProvider,
|
1970
|
+
)
|
1971
|
+
|
1972
|
+
@classmethod
|
1973
|
+
def bind(
|
1974
|
+
cls,
|
1975
|
+
obj: ta.Any,
|
1976
|
+
*,
|
1977
|
+
key: ta.Any = None,
|
1978
|
+
tag: ta.Any = None,
|
1979
|
+
array: ta.Optional[bool] = None, # noqa
|
1980
|
+
|
1981
|
+
to_fn: ta.Any = None,
|
1982
|
+
to_ctor: ta.Any = None,
|
1983
|
+
to_const: ta.Any = None,
|
1984
|
+
to_key: ta.Any = None,
|
1985
|
+
|
1986
|
+
singleton: bool = False,
|
1987
|
+
) -> InjectorBinding:
|
1988
|
+
if obj is None or obj is inspect.Parameter.empty:
|
1989
|
+
raise TypeError(obj)
|
1990
|
+
if isinstance(obj, cls._BANNED_BIND_TYPES):
|
1991
|
+
raise TypeError(obj)
|
1992
|
+
|
1993
|
+
##
|
1994
|
+
|
1995
|
+
if key is not None:
|
1996
|
+
key = as_injector_key(key)
|
1997
|
+
|
1998
|
+
##
|
1999
|
+
|
2000
|
+
has_to = (
|
2001
|
+
to_fn is not None or
|
2002
|
+
to_ctor is not None or
|
2003
|
+
to_const is not None or
|
2004
|
+
to_key is not None
|
2005
|
+
)
|
2006
|
+
if isinstance(obj, InjectorKey):
|
2007
|
+
if key is None:
|
2008
|
+
key = obj
|
2009
|
+
elif isinstance(obj, type):
|
2010
|
+
if not has_to:
|
2011
|
+
to_ctor = obj
|
2012
|
+
if key is None:
|
2013
|
+
key = InjectorKey(obj)
|
2014
|
+
elif cls._is_fn(obj) and not has_to:
|
2015
|
+
to_fn = obj
|
2016
|
+
if key is None:
|
2017
|
+
sig = _injection_signature(obj)
|
2018
|
+
ty = check_isinstance(sig.return_annotation, type)
|
2019
|
+
key = InjectorKey(ty)
|
2020
|
+
else:
|
2021
|
+
if to_const is not None:
|
2022
|
+
raise TypeError('Cannot bind instance with to_const')
|
2023
|
+
to_const = obj
|
2024
|
+
if key is None:
|
2025
|
+
key = InjectorKey(type(obj))
|
2026
|
+
del has_to
|
2027
|
+
|
2028
|
+
##
|
2029
|
+
|
2030
|
+
if tag is not None:
|
2031
|
+
if key.tag is not None:
|
2032
|
+
raise TypeError('Tag already set')
|
2033
|
+
key = dc.replace(key, tag=tag)
|
2034
|
+
|
2035
|
+
if array is not None:
|
2036
|
+
key = dc.replace(key, array=array)
|
2037
|
+
|
2038
|
+
##
|
2039
|
+
|
2040
|
+
providers: ta.List[InjectorProvider] = []
|
2041
|
+
if to_fn is not None:
|
2042
|
+
providers.append(FnInjectorProvider(to_fn))
|
2043
|
+
if to_ctor is not None:
|
2044
|
+
providers.append(CtorInjectorProvider(to_ctor))
|
2045
|
+
if to_const is not None:
|
2046
|
+
providers.append(ConstInjectorProvider(to_const))
|
2047
|
+
if to_key is not None:
|
2048
|
+
providers.append(LinkInjectorProvider(as_injector_key(to_key)))
|
2049
|
+
if not providers:
|
2050
|
+
raise TypeError('Must specify provider')
|
2051
|
+
if len(providers) > 1:
|
2052
|
+
raise TypeError('May not specify multiple providers')
|
2053
|
+
provider, = providers
|
2054
|
+
|
2055
|
+
##
|
2056
|
+
|
2057
|
+
if singleton:
|
2058
|
+
provider = SingletonInjectorProvider(provider)
|
2059
|
+
|
2060
|
+
##
|
2061
|
+
|
2062
|
+
binding = InjectorBinding(key, provider)
|
2063
|
+
|
2064
|
+
##
|
2065
|
+
|
2066
|
+
return binding
|
681
2067
|
|
682
|
-
priority: int = 999
|
683
2068
|
|
684
|
-
|
2069
|
+
###
|
2070
|
+
# injector
|
685
2071
|
|
686
2072
|
|
687
|
-
|
688
|
-
class ServerConfig:
|
689
|
-
user: ta.Optional[str] = None
|
690
|
-
nodaemon: bool = False
|
691
|
-
umask: int = 0o22
|
692
|
-
directory: ta.Optional[str] = None
|
693
|
-
logfile: str = 'supervisord.log'
|
694
|
-
logfile_maxbytes: int = 50 * 1024 * 1024
|
695
|
-
logfile_backups: int = 10
|
696
|
-
loglevel: int = logging.INFO
|
697
|
-
pidfile: str = 'supervisord.pid'
|
698
|
-
identifier: str = 'supervisor'
|
699
|
-
child_logdir: str = '/dev/null'
|
700
|
-
minfds: int = 1024
|
701
|
-
minprocs: int = 200
|
702
|
-
nocleanup: bool = False
|
703
|
-
strip_ansi: bool = False
|
704
|
-
silent: bool = False
|
2073
|
+
_INJECTOR_INJECTOR_KEY = InjectorKey(Injector)
|
705
2074
|
|
706
|
-
groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
|
707
2075
|
|
708
|
-
|
709
|
-
def
|
710
|
-
|
711
|
-
umask: ta.Union[int, str] = 0o22,
|
712
|
-
directory: ta.Optional[str] = None,
|
713
|
-
logfile: str = 'supervisord.log',
|
714
|
-
logfile_maxbytes: ta.Union[int, str] = 50 * 1024 * 1024,
|
715
|
-
loglevel: ta.Union[int, str] = logging.INFO,
|
716
|
-
pidfile: str = 'supervisord.pid',
|
717
|
-
child_logdir: ta.Optional[str] = None,
|
718
|
-
**kwargs: ta.Any,
|
719
|
-
) -> 'ServerConfig':
|
720
|
-
return cls(
|
721
|
-
umask=octal_type(umask),
|
722
|
-
directory=existing_directory(directory) if directory is not None else None,
|
723
|
-
logfile=existing_dirpath(logfile),
|
724
|
-
logfile_maxbytes=byte_size(logfile_maxbytes),
|
725
|
-
loglevel=logging_level(loglevel),
|
726
|
-
pidfile=existing_dirpath(pidfile),
|
727
|
-
child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
|
728
|
-
**kwargs,
|
729
|
-
)
|
2076
|
+
class _Injector(Injector):
|
2077
|
+
def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
|
2078
|
+
super().__init__()
|
730
2079
|
|
2080
|
+
self._bs = check_isinstance(bs, InjectorBindings)
|
2081
|
+
self._p: ta.Optional[Injector] = check_isinstance(p, (Injector, type(None)))
|
731
2082
|
|
732
|
-
|
733
|
-
# ../states.py
|
2083
|
+
self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
|
734
2084
|
|
2085
|
+
if _INJECTOR_INJECTOR_KEY in self._pfm:
|
2086
|
+
raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
|
735
2087
|
|
736
|
-
|
2088
|
+
def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
|
2089
|
+
key = as_injector_key(key)
|
737
2090
|
|
2091
|
+
if key == _INJECTOR_INJECTOR_KEY:
|
2092
|
+
return Maybe.just(self)
|
738
2093
|
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
if not name.startswith('__'):
|
743
|
-
code = getattr(states, name)
|
744
|
-
d[code] = name
|
745
|
-
return d
|
2094
|
+
fn = self._pfm.get(key)
|
2095
|
+
if fn is not None:
|
2096
|
+
return Maybe.just(fn(self))
|
746
2097
|
|
2098
|
+
if self._p is not None:
|
2099
|
+
pv = self._p.try_provide(key)
|
2100
|
+
if pv is not None:
|
2101
|
+
return Maybe.empty()
|
747
2102
|
|
748
|
-
|
2103
|
+
return Maybe.empty()
|
749
2104
|
|
2105
|
+
def provide(self, key: ta.Any) -> ta.Any:
|
2106
|
+
v = self.try_provide(key)
|
2107
|
+
if v.present:
|
2108
|
+
return v.must()
|
2109
|
+
raise UnboundInjectorKeyError(key)
|
750
2110
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
2111
|
+
def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
|
2112
|
+
kt = build_injection_kwargs_target(obj)
|
2113
|
+
ret: ta.Dict[str, ta.Any] = {}
|
2114
|
+
for kw in kt.kwargs:
|
2115
|
+
if kw.has_default:
|
2116
|
+
if not (mv := self.try_provide(kw.key)).present:
|
2117
|
+
continue
|
2118
|
+
v = mv.must()
|
2119
|
+
else:
|
2120
|
+
v = self.provide(kw.key)
|
2121
|
+
ret[kw.name] = v
|
2122
|
+
return ret
|
760
2123
|
|
2124
|
+
def inject(self, obj: ta.Any) -> ta.Any:
|
2125
|
+
kws = self.provide_kwargs(obj)
|
2126
|
+
return obj(**kws)
|
761
2127
|
|
762
|
-
STOPPED_STATES = (
|
763
|
-
ProcessStates.STOPPED,
|
764
|
-
ProcessStates.EXITED,
|
765
|
-
ProcessStates.FATAL,
|
766
|
-
ProcessStates.UNKNOWN,
|
767
|
-
)
|
768
2128
|
|
769
|
-
|
770
|
-
|
771
|
-
ProcessStates.BACKOFF,
|
772
|
-
ProcessStates.STARTING,
|
773
|
-
)
|
2129
|
+
###
|
2130
|
+
# injection helpers
|
774
2131
|
|
775
|
-
SIGNALLABLE_STATES = (
|
776
|
-
ProcessStates.RUNNING,
|
777
|
-
ProcessStates.STARTING,
|
778
|
-
ProcessStates.STOPPING,
|
779
|
-
)
|
780
2132
|
|
2133
|
+
class Injection:
|
2134
|
+
def __new__(cls, *args, **kwargs): # noqa
|
2135
|
+
raise TypeError
|
781
2136
|
|
782
|
-
|
2137
|
+
# keys
|
783
2138
|
|
2139
|
+
@classmethod
|
2140
|
+
def as_key(cls, o: ta.Any) -> InjectorKey:
|
2141
|
+
return as_injector_key(o)
|
784
2142
|
|
785
|
-
|
786
|
-
|
2143
|
+
@classmethod
|
2144
|
+
def array(cls, o: ta.Any) -> InjectorKey:
|
2145
|
+
return dc.replace(as_injector_key(o), array=True)
|
787
2146
|
|
2147
|
+
@classmethod
|
2148
|
+
def tag(cls, o: ta.Any, t: ta.Any) -> InjectorKey:
|
2149
|
+
return dc.replace(as_injector_key(o), tag=t)
|
788
2150
|
|
789
|
-
|
2151
|
+
# bindings
|
790
2152
|
|
2153
|
+
@classmethod
|
2154
|
+
def as_bindings(cls, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
2155
|
+
return as_injector_bindings(*args)
|
791
2156
|
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
RESTARTING = 0
|
796
|
-
SHUTDOWN = -1
|
2157
|
+
@classmethod
|
2158
|
+
def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
2159
|
+
return injector_override(p, *args)
|
797
2160
|
|
2161
|
+
# binder
|
798
2162
|
|
799
|
-
|
2163
|
+
@classmethod
|
2164
|
+
def bind(
|
2165
|
+
cls,
|
2166
|
+
obj: ta.Any,
|
2167
|
+
*,
|
2168
|
+
key: ta.Any = None,
|
2169
|
+
tag: ta.Any = None,
|
2170
|
+
array: ta.Optional[bool] = None, # noqa
|
2171
|
+
|
2172
|
+
to_fn: ta.Any = None,
|
2173
|
+
to_ctor: ta.Any = None,
|
2174
|
+
to_const: ta.Any = None,
|
2175
|
+
to_key: ta.Any = None,
|
2176
|
+
|
2177
|
+
singleton: bool = False,
|
2178
|
+
) -> InjectorBinding:
|
2179
|
+
return InjectorBinder.bind(
|
2180
|
+
obj,
|
2181
|
+
|
2182
|
+
key=key,
|
2183
|
+
tag=tag,
|
2184
|
+
array=array,
|
2185
|
+
|
2186
|
+
to_fn=to_fn,
|
2187
|
+
to_ctor=to_ctor,
|
2188
|
+
to_const=to_const,
|
2189
|
+
to_key=to_key,
|
2190
|
+
|
2191
|
+
singleton=singleton,
|
2192
|
+
)
|
800
2193
|
|
2194
|
+
# injector
|
801
2195
|
|
802
|
-
|
803
|
-
|
2196
|
+
@classmethod
|
2197
|
+
def create_injector(cls, *args: InjectorBindingOrBindings, p: ta.Optional[Injector] = None) -> Injector:
|
2198
|
+
return _Injector(as_injector_bindings(*args), p)
|
2199
|
+
|
2200
|
+
|
2201
|
+
inj = Injection
|
804
2202
|
|
805
2203
|
|
806
2204
|
########################################
|
@@ -1575,6 +2973,66 @@ def unmarshal_obj(o: ta.Any, ty: ta.Union[ta.Type[T], ta.Any]) -> T:
|
|
1575
2973
|
return get_obj_marshaler(ty).unmarshal(o)
|
1576
2974
|
|
1577
2975
|
|
2976
|
+
########################################
|
2977
|
+
# ../../configs.py
|
2978
|
+
|
2979
|
+
|
2980
|
+
def read_config_file(
|
2981
|
+
path: str,
|
2982
|
+
cls: ta.Type[T],
|
2983
|
+
*,
|
2984
|
+
prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
|
2985
|
+
) -> T:
|
2986
|
+
with open(path) as cf:
|
2987
|
+
if path.endswith('.toml'):
|
2988
|
+
config_dct = toml_loads(cf.read())
|
2989
|
+
else:
|
2990
|
+
config_dct = json.loads(cf.read())
|
2991
|
+
|
2992
|
+
if prepare is not None:
|
2993
|
+
config_dct = prepare(config_dct) # type: ignore
|
2994
|
+
|
2995
|
+
return unmarshal_obj(config_dct, cls)
|
2996
|
+
|
2997
|
+
|
2998
|
+
def build_config_named_children(
|
2999
|
+
o: ta.Union[
|
3000
|
+
ta.Sequence[ConfigMapping],
|
3001
|
+
ta.Mapping[str, ConfigMapping],
|
3002
|
+
None,
|
3003
|
+
],
|
3004
|
+
*,
|
3005
|
+
name_key: str = 'name',
|
3006
|
+
) -> ta.Optional[ta.Sequence[ConfigMapping]]:
|
3007
|
+
if o is None:
|
3008
|
+
return None
|
3009
|
+
|
3010
|
+
lst: ta.List[ConfigMapping] = []
|
3011
|
+
if isinstance(o, ta.Mapping):
|
3012
|
+
for k, v in o.items():
|
3013
|
+
check_isinstance(v, ta.Mapping)
|
3014
|
+
if name_key in v:
|
3015
|
+
n = v[name_key]
|
3016
|
+
if k != n:
|
3017
|
+
raise KeyError(f'Given names do not match: {n} != {k}')
|
3018
|
+
lst.append(v)
|
3019
|
+
else:
|
3020
|
+
lst.append({name_key: k, **v})
|
3021
|
+
|
3022
|
+
else:
|
3023
|
+
check_not_isinstance(o, str)
|
3024
|
+
lst.extend(o)
|
3025
|
+
|
3026
|
+
seen = set()
|
3027
|
+
for d in lst:
|
3028
|
+
n = d['name']
|
3029
|
+
if n in d:
|
3030
|
+
raise KeyError(f'Duplicate name: {n}')
|
3031
|
+
seen.add(n)
|
3032
|
+
|
3033
|
+
return lst
|
3034
|
+
|
3035
|
+
|
1578
3036
|
########################################
|
1579
3037
|
# ../events.py
|
1580
3038
|
|
@@ -1602,12 +3060,9 @@ class EventCallbacks:
|
|
1602
3060
|
|
1603
3061
|
EVENT_CALLBACKS = EventCallbacks()
|
1604
3062
|
|
1605
|
-
notify_event = EVENT_CALLBACKS.notify
|
1606
|
-
clear_events = EVENT_CALLBACKS.clear
|
1607
|
-
|
1608
3063
|
|
1609
3064
|
class Event(abc.ABC): # noqa
|
1610
|
-
"""Abstract event type
|
3065
|
+
"""Abstract event type."""
|
1611
3066
|
|
1612
3067
|
|
1613
3068
|
class ProcessLogEvent(Event, abc.ABC):
|
@@ -1687,7 +3142,7 @@ class RemoteCommunicationEvent(Event):
|
|
1687
3142
|
|
1688
3143
|
|
1689
3144
|
class SupervisorStateChangeEvent(Event):
|
1690
|
-
"""
|
3145
|
+
"""Abstract class."""
|
1691
3146
|
|
1692
3147
|
def payload(self):
|
1693
3148
|
return ''
|
@@ -1709,7 +3164,7 @@ class EventRejectedEvent: # purposely does not subclass Event
|
|
1709
3164
|
|
1710
3165
|
|
1711
3166
|
class ProcessStateEvent(Event):
|
1712
|
-
"""
|
3167
|
+
"""Abstract class, never raised directly."""
|
1713
3168
|
frm = None
|
1714
3169
|
to = None
|
1715
3170
|
|
@@ -1798,7 +3253,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent):
|
|
1798
3253
|
|
1799
3254
|
|
1800
3255
|
class TickEvent(Event):
|
1801
|
-
"""
|
3256
|
+
"""Abstract."""
|
1802
3257
|
|
1803
3258
|
def __init__(self, when, supervisord):
|
1804
3259
|
super().__init__()
|
@@ -2080,34 +3535,155 @@ if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
|
|
2080
3535
|
|
2081
3536
|
return readables, writables
|
2082
3537
|
|
2083
|
-
def before_daemonize(self) -> None:
|
2084
|
-
self.close()
|
3538
|
+
def before_daemonize(self) -> None:
|
3539
|
+
self.close()
|
3540
|
+
|
3541
|
+
def after_daemonize(self) -> None:
|
3542
|
+
self._kqueue = select.kqueue()
|
3543
|
+
for fd in self._readables:
|
3544
|
+
self.register_readable(fd)
|
3545
|
+
for fd in self._writables:
|
3546
|
+
self.register_writable(fd)
|
3547
|
+
|
3548
|
+
def close(self) -> None:
|
3549
|
+
self._kqueue.close() # type: ignore
|
3550
|
+
self._kqueue = None
|
3551
|
+
|
3552
|
+
else:
|
3553
|
+
KqueuePoller = None
|
3554
|
+
|
3555
|
+
|
3556
|
+
Poller: ta.Type[BasePoller]
|
3557
|
+
if (
|
3558
|
+
sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
|
3559
|
+
hasattr(select, 'kqueue') and KqueuePoller is not None
|
3560
|
+
):
|
3561
|
+
Poller = KqueuePoller
|
3562
|
+
elif hasattr(select, 'poll'):
|
3563
|
+
Poller = PollPoller
|
3564
|
+
else:
|
3565
|
+
Poller = SelectPoller
|
3566
|
+
|
3567
|
+
|
3568
|
+
########################################
|
3569
|
+
# ../configs.py
|
3570
|
+
|
3571
|
+
|
3572
|
+
##
|
3573
|
+
|
3574
|
+
|
3575
|
+
@dc.dataclass(frozen=True)
|
3576
|
+
class ProcessConfig:
|
3577
|
+
name: str
|
3578
|
+
command: str
|
3579
|
+
|
3580
|
+
uid: ta.Optional[int] = None
|
3581
|
+
directory: ta.Optional[str] = None
|
3582
|
+
umask: ta.Optional[int] = None
|
3583
|
+
priority: int = 999
|
3584
|
+
|
3585
|
+
autostart: bool = True
|
3586
|
+
autorestart: str = 'unexpected'
|
3587
|
+
|
3588
|
+
startsecs: int = 1
|
3589
|
+
startretries: int = 3
|
3590
|
+
|
3591
|
+
numprocs: int = 1
|
3592
|
+
numprocs_start: int = 0
|
3593
|
+
|
3594
|
+
@dc.dataclass(frozen=True)
|
3595
|
+
class Log:
|
3596
|
+
file: ta.Optional[str] = None
|
3597
|
+
capture_maxbytes: ta.Optional[int] = None
|
3598
|
+
events_enabled: bool = False
|
3599
|
+
syslog: bool = False
|
3600
|
+
backups: ta.Optional[int] = None
|
3601
|
+
maxbytes: ta.Optional[int] = None
|
3602
|
+
|
3603
|
+
stdout: Log = Log()
|
3604
|
+
stderr: Log = Log()
|
3605
|
+
|
3606
|
+
stopsignal: int = signal.SIGTERM
|
3607
|
+
stopwaitsecs: int = 10
|
3608
|
+
stopasgroup: bool = False
|
3609
|
+
|
3610
|
+
killasgroup: bool = False
|
3611
|
+
|
3612
|
+
exitcodes: ta.Sequence[int] = (0,)
|
3613
|
+
|
3614
|
+
redirect_stderr: bool = False
|
3615
|
+
|
3616
|
+
environment: ta.Optional[ta.Mapping[str, str]] = None
|
3617
|
+
|
3618
|
+
|
3619
|
+
@dc.dataclass(frozen=True)
|
3620
|
+
class ProcessGroupConfig:
|
3621
|
+
name: str
|
3622
|
+
|
3623
|
+
priority: int = 999
|
3624
|
+
|
3625
|
+
processes: ta.Optional[ta.Sequence[ProcessConfig]] = None
|
3626
|
+
|
3627
|
+
|
3628
|
+
@dc.dataclass(frozen=True)
|
3629
|
+
class ServerConfig:
|
3630
|
+
user: ta.Optional[str] = None
|
3631
|
+
nodaemon: bool = False
|
3632
|
+
umask: int = 0o22
|
3633
|
+
directory: ta.Optional[str] = None
|
3634
|
+
logfile: str = 'supervisord.log'
|
3635
|
+
logfile_maxbytes: int = 50 * 1024 * 1024
|
3636
|
+
logfile_backups: int = 10
|
3637
|
+
loglevel: int = logging.INFO
|
3638
|
+
pidfile: str = 'supervisord.pid'
|
3639
|
+
identifier: str = 'supervisor'
|
3640
|
+
child_logdir: str = '/dev/null'
|
3641
|
+
minfds: int = 1024
|
3642
|
+
minprocs: int = 200
|
3643
|
+
nocleanup: bool = False
|
3644
|
+
strip_ansi: bool = False
|
3645
|
+
silent: bool = False
|
3646
|
+
|
3647
|
+
groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
|
3648
|
+
|
3649
|
+
@classmethod
|
3650
|
+
def new(
|
3651
|
+
cls,
|
3652
|
+
umask: ta.Union[int, str] = 0o22,
|
3653
|
+
directory: ta.Optional[str] = None,
|
3654
|
+
logfile: str = 'supervisord.log',
|
3655
|
+
logfile_maxbytes: ta.Union[int, str] = 50 * 1024 * 1024,
|
3656
|
+
loglevel: ta.Union[int, str] = logging.INFO,
|
3657
|
+
pidfile: str = 'supervisord.pid',
|
3658
|
+
child_logdir: ta.Optional[str] = None,
|
3659
|
+
**kwargs: ta.Any,
|
3660
|
+
) -> 'ServerConfig':
|
3661
|
+
return cls(
|
3662
|
+
umask=octal_type(umask),
|
3663
|
+
directory=existing_directory(directory) if directory is not None else None,
|
3664
|
+
logfile=existing_dirpath(logfile),
|
3665
|
+
logfile_maxbytes=byte_size(logfile_maxbytes),
|
3666
|
+
loglevel=logging_level(loglevel),
|
3667
|
+
pidfile=existing_dirpath(pidfile),
|
3668
|
+
child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
|
3669
|
+
**kwargs,
|
3670
|
+
)
|
3671
|
+
|
2085
3672
|
|
2086
|
-
|
2087
|
-
self._kqueue = select.kqueue()
|
2088
|
-
for fd in self._readables:
|
2089
|
-
self.register_readable(fd)
|
2090
|
-
for fd in self._writables:
|
2091
|
-
self.register_writable(fd)
|
3673
|
+
##
|
2092
3674
|
|
2093
|
-
def close(self) -> None:
|
2094
|
-
self._kqueue.close() # type: ignore
|
2095
|
-
self._kqueue = None
|
2096
3675
|
|
2097
|
-
|
2098
|
-
|
3676
|
+
def prepare_process_group_config(dct: ConfigMapping) -> ConfigMapping:
|
3677
|
+
out = dict(dct)
|
3678
|
+
out['processes'] = build_config_named_children(out.get('processes'))
|
3679
|
+
return out
|
2099
3680
|
|
2100
3681
|
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
Poller = KqueuePoller
|
2107
|
-
elif hasattr(select, 'poll'):
|
2108
|
-
Poller = PollPoller
|
2109
|
-
else:
|
2110
|
-
Poller = SelectPoller
|
3682
|
+
def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
|
3683
|
+
out = dict(dct)
|
3684
|
+
group_dcts = build_config_named_children(out.get('groups'))
|
3685
|
+
out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
|
3686
|
+
return out
|
2111
3687
|
|
2112
3688
|
|
2113
3689
|
########################################
|
@@ -2134,6 +3710,11 @@ class AbstractServerContext(abc.ABC):
|
|
2134
3710
|
def pid_history(self) -> ta.Dict[int, 'AbstractSubprocess']:
|
2135
3711
|
raise NotImplementedError
|
2136
3712
|
|
3713
|
+
@property
|
3714
|
+
@abc.abstractmethod
|
3715
|
+
def inherited_fds(self) -> ta.FrozenSet[int]:
|
3716
|
+
raise NotImplementedError
|
3717
|
+
|
2137
3718
|
|
2138
3719
|
class AbstractSubprocess(abc.ABC):
|
2139
3720
|
@property
|
@@ -2165,12 +3746,14 @@ class ServerContext(AbstractServerContext):
|
|
2165
3746
|
self,
|
2166
3747
|
config: ServerConfig,
|
2167
3748
|
*,
|
2168
|
-
epoch:
|
3749
|
+
epoch: ServerEpoch = ServerEpoch(0),
|
3750
|
+
inherited_fds: ta.Optional[InheritedFds] = None,
|
2169
3751
|
) -> None:
|
2170
3752
|
super().__init__()
|
2171
3753
|
|
2172
3754
|
self._config = config
|
2173
3755
|
self._epoch = epoch
|
3756
|
+
self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
|
2174
3757
|
|
2175
3758
|
self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
|
2176
3759
|
self._state: SupervisorState = SupervisorStates.RUNNING
|
@@ -2194,7 +3777,7 @@ class ServerContext(AbstractServerContext):
|
|
2194
3777
|
return self._config
|
2195
3778
|
|
2196
3779
|
@property
|
2197
|
-
def epoch(self) ->
|
3780
|
+
def epoch(self) -> ServerEpoch:
|
2198
3781
|
return self._epoch
|
2199
3782
|
|
2200
3783
|
@property
|
@@ -2224,6 +3807,10 @@ class ServerContext(AbstractServerContext):
|
|
2224
3807
|
def gid(self) -> ta.Optional[int]:
|
2225
3808
|
return self._gid
|
2226
3809
|
|
3810
|
+
@property
|
3811
|
+
def inherited_fds(self) -> InheritedFds:
|
3812
|
+
return self._inherited_fds
|
3813
|
+
|
2227
3814
|
##
|
2228
3815
|
|
2229
3816
|
def set_signals(self) -> None:
|
@@ -2752,10 +4339,10 @@ class OutputDispatcher(Dispatcher):
|
|
2752
4339
|
|
2753
4340
|
if self._channel == 'stdout':
|
2754
4341
|
if self._stdout_events_enabled:
|
2755
|
-
|
4342
|
+
EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
2756
4343
|
|
2757
4344
|
elif self._stderr_events_enabled:
|
2758
|
-
|
4345
|
+
EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
2759
4346
|
|
2760
4347
|
def record_output(self):
|
2761
4348
|
if self._capture_log is None:
|
@@ -2806,7 +4393,7 @@ class OutputDispatcher(Dispatcher):
|
|
2806
4393
|
channel = self._channel
|
2807
4394
|
procname = self._process.config.name
|
2808
4395
|
event = self.event_type(self._process, self._process.pid, data)
|
2809
|
-
|
4396
|
+
EVENT_CALLBACKS.notify(event)
|
2810
4397
|
|
2811
4398
|
log.debug('%r %s emitted a comm event', procname, channel)
|
2812
4399
|
for handler in self._capture_log.handlers:
|
@@ -2867,6 +4454,9 @@ class InputDispatcher(Dispatcher):
|
|
2867
4454
|
# ../process.py
|
2868
4455
|
|
2869
4456
|
|
4457
|
+
##
|
4458
|
+
|
4459
|
+
|
2870
4460
|
@functools.total_ordering
|
2871
4461
|
class Subprocess(AbstractSubprocess):
|
2872
4462
|
"""A class to manage a subprocess."""
|
@@ -2892,7 +4482,12 @@ class Subprocess(AbstractSubprocess):
|
|
2892
4482
|
spawn_err = None # error message attached by spawn() if any
|
2893
4483
|
group = None # ProcessGroup instance if process is in the group
|
2894
4484
|
|
2895
|
-
def __init__(
|
4485
|
+
def __init__(
|
4486
|
+
self,
|
4487
|
+
config: ProcessConfig,
|
4488
|
+
group: 'ProcessGroup',
|
4489
|
+
context: AbstractServerContext,
|
4490
|
+
) -> None:
|
2896
4491
|
super().__init__()
|
2897
4492
|
self._config = config
|
2898
4493
|
self.group = group
|
@@ -3019,7 +4614,7 @@ class Subprocess(AbstractSubprocess):
|
|
3019
4614
|
event_class = self.event_map.get(new_state)
|
3020
4615
|
if event_class is not None:
|
3021
4616
|
event = event_class(self, old_state, expected)
|
3022
|
-
|
4617
|
+
EVENT_CALLBACKS.notify(event)
|
3023
4618
|
|
3024
4619
|
return True
|
3025
4620
|
|
@@ -3136,7 +4731,10 @@ class Subprocess(AbstractSubprocess):
|
|
3136
4731
|
os.dup2(self._pipes['child_stdout'], 2)
|
3137
4732
|
else:
|
3138
4733
|
os.dup2(self._pipes['child_stderr'], 2)
|
4734
|
+
|
3139
4735
|
for i in range(3, self.context.config.minfds):
|
4736
|
+
if i in self.context.inherited_fds:
|
4737
|
+
continue
|
3140
4738
|
close_fd(i)
|
3141
4739
|
|
3142
4740
|
def _spawn_as_child(self, filename: str, argv: ta.Sequence[str]) -> None:
|
@@ -3171,7 +4769,7 @@ class Subprocess(AbstractSubprocess):
|
|
3171
4769
|
cwd = self.config.directory
|
3172
4770
|
try:
|
3173
4771
|
if cwd is not None:
|
3174
|
-
os.chdir(cwd)
|
4772
|
+
os.chdir(os.path.expanduser(cwd))
|
3175
4773
|
except OSError as why:
|
3176
4774
|
code = errno.errorcode.get(why.args[0], why.args[0])
|
3177
4775
|
msg = f"couldn't chdir to {cwd}: {code}\n"
|
@@ -3227,7 +4825,7 @@ class Subprocess(AbstractSubprocess):
|
|
3227
4825
|
return self.kill(self.config.stopsignal)
|
3228
4826
|
|
3229
4827
|
def stop_report(self) -> None:
|
3230
|
-
"""
|
4828
|
+
"""Log a 'waiting for x to stop' message with throttling."""
|
3231
4829
|
if self.state == ProcessStates.STOPPING:
|
3232
4830
|
now = time.time()
|
3233
4831
|
|
@@ -3357,7 +4955,7 @@ class Subprocess(AbstractSubprocess):
|
|
3357
4955
|
return None
|
3358
4956
|
|
3359
4957
|
def finish(self, sts: int) -> None:
|
3360
|
-
"""
|
4958
|
+
"""The process was reaped and we need to report and manage its state."""
|
3361
4959
|
|
3362
4960
|
self.drain()
|
3363
4961
|
|
@@ -3438,7 +5036,7 @@ class Subprocess(AbstractSubprocess):
|
|
3438
5036
|
# system that this event was rejected so it can be processed again.
|
3439
5037
|
if self.event is not None:
|
3440
5038
|
# Note: this should only be true if we were in the BUSY state when finish() was called.
|
3441
|
-
|
5039
|
+
EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
|
3442
5040
|
self.event = None
|
3443
5041
|
|
3444
5042
|
def set_uid(self) -> ta.Optional[str]:
|
@@ -3530,15 +5128,39 @@ class Subprocess(AbstractSubprocess):
|
|
3530
5128
|
pass
|
3531
5129
|
|
3532
5130
|
|
5131
|
+
##
|
5132
|
+
|
5133
|
+
|
5134
|
+
@dc.dataclass(frozen=True)
|
5135
|
+
class SubprocessFactory:
|
5136
|
+
fn: ta.Callable[[ProcessConfig, 'ProcessGroup'], Subprocess]
|
5137
|
+
|
5138
|
+
def __call__(self, config: ProcessConfig, group: 'ProcessGroup') -> Subprocess:
|
5139
|
+
return self.fn(config, group)
|
5140
|
+
|
5141
|
+
|
3533
5142
|
@functools.total_ordering
|
3534
5143
|
class ProcessGroup:
|
3535
|
-
def __init__(
|
5144
|
+
def __init__(
|
5145
|
+
self,
|
5146
|
+
config: ProcessGroupConfig,
|
5147
|
+
context: ServerContext,
|
5148
|
+
*,
|
5149
|
+
subprocess_factory: ta.Optional[SubprocessFactory] = None,
|
5150
|
+
):
|
3536
5151
|
super().__init__()
|
3537
5152
|
self.config = config
|
3538
5153
|
self.context = context
|
5154
|
+
|
5155
|
+
if subprocess_factory is None:
|
5156
|
+
def make_subprocess(config: ProcessConfig, group: ProcessGroup) -> Subprocess:
|
5157
|
+
return Subprocess(config, group, self.context)
|
5158
|
+
subprocess_factory = SubprocessFactory(make_subprocess)
|
5159
|
+
self._subprocess_factory = subprocess_factory
|
5160
|
+
|
3539
5161
|
self.processes = {}
|
3540
5162
|
for pconfig in self.config.processes or []:
|
3541
|
-
process =
|
5163
|
+
process = self._subprocess_factory(pconfig, self)
|
3542
5164
|
self.processes[pconfig.name] = process
|
3543
5165
|
|
3544
5166
|
def __lt__(self, other):
|
@@ -3604,22 +5226,44 @@ class ProcessGroup:
|
|
3604
5226
|
# ../supervisor.py
|
3605
5227
|
|
3606
5228
|
|
3607
|
-
def timeslice(period, when):
|
5229
|
+
def timeslice(period: int, when: float) -> int:
|
3608
5230
|
return int(when - (when % period))
|
3609
5231
|
|
3610
5232
|
|
5233
|
+
@dc.dataclass(frozen=True)
|
5234
|
+
class ProcessGroupFactory:
|
5235
|
+
fn: ta.Callable[[ProcessGroupConfig], ProcessGroup]
|
5236
|
+
|
5237
|
+
def __call__(self, config: ProcessGroupConfig) -> ProcessGroup:
|
5238
|
+
return self.fn(config)
|
5239
|
+
|
5240
|
+
|
3611
5241
|
class Supervisor:
|
3612
5242
|
|
3613
|
-
def __init__(
|
5243
|
+
def __init__(
|
5244
|
+
self,
|
5245
|
+
context: ServerContext,
|
5246
|
+
*,
|
5247
|
+
process_group_factory: ta.Optional[ProcessGroupFactory] = None,
|
5248
|
+
) -> None:
|
3614
5249
|
super().__init__()
|
3615
5250
|
|
3616
5251
|
self._context = context
|
5252
|
+
|
5253
|
+
if process_group_factory is None:
|
5254
|
+
def make_process_group(config: ProcessGroupConfig) -> ProcessGroup:
|
5255
|
+
return ProcessGroup(config, self._context)
|
5256
|
+
process_group_factory = ProcessGroupFactory(make_process_group)
|
5257
|
+
self._process_group_factory = process_group_factory
|
5258
|
+
|
3617
5259
|
self._ticks: ta.Dict[int, float] = {}
|
3618
5260
|
self._process_groups: ta.Dict[str, ProcessGroup] = {} # map of process group name to process group object
|
3619
5261
|
self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
|
3620
5262
|
self._stopping = False # set after we detect that we are handling a stop request
|
3621
5263
|
self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
|
3622
5264
|
|
5265
|
+
#
|
5266
|
+
|
3623
5267
|
@property
|
3624
5268
|
def context(self) -> ServerContext:
|
3625
5269
|
return self._context
|
@@ -3627,58 +5271,7 @@ class Supervisor:
|
|
3627
5271
|
def get_state(self) -> SupervisorState:
|
3628
5272
|
return self._context.state
|
3629
5273
|
|
3630
|
-
|
3631
|
-
self.setup()
|
3632
|
-
self.run()
|
3633
|
-
|
3634
|
-
@cached_nullary
|
3635
|
-
def setup(self) -> None:
|
3636
|
-
if not self._context.first:
|
3637
|
-
# prevent crash on libdispatch-based systems, at least for the first request
|
3638
|
-
self._context.cleanup_fds()
|
3639
|
-
|
3640
|
-
self._context.set_uid_or_exit()
|
3641
|
-
|
3642
|
-
if self._context.first:
|
3643
|
-
self._context.set_rlimits_or_exit()
|
3644
|
-
|
3645
|
-
# this sets the options.logger object delay logger instantiation until after setuid
|
3646
|
-
if not self._context.config.nocleanup:
|
3647
|
-
# clean up old automatic logs
|
3648
|
-
self._context.clear_auto_child_logdir()
|
3649
|
-
|
3650
|
-
def run(
|
3651
|
-
self,
|
3652
|
-
*,
|
3653
|
-
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
3654
|
-
) -> None:
|
3655
|
-
self._process_groups = {} # clear
|
3656
|
-
self._stop_groups = None # clear
|
3657
|
-
|
3658
|
-
clear_events()
|
3659
|
-
|
3660
|
-
try:
|
3661
|
-
for config in self._context.config.groups or []:
|
3662
|
-
self.add_process_group(config)
|
3663
|
-
|
3664
|
-
self._context.set_signals()
|
3665
|
-
|
3666
|
-
if not self._context.config.nodaemon and self._context.first:
|
3667
|
-
self._context.daemonize()
|
3668
|
-
|
3669
|
-
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
3670
|
-
self._context.write_pidfile()
|
3671
|
-
|
3672
|
-
notify_event(SupervisorRunningEvent())
|
3673
|
-
|
3674
|
-
while True:
|
3675
|
-
if callback is not None and not callback(self):
|
3676
|
-
break
|
3677
|
-
|
3678
|
-
self._run_once()
|
3679
|
-
|
3680
|
-
finally:
|
3681
|
-
self._context.cleanup()
|
5274
|
+
#
|
3682
5275
|
|
3683
5276
|
class DiffToActive(ta.NamedTuple):
|
3684
5277
|
added: ta.List[ProcessGroupConfig]
|
@@ -3704,10 +5297,10 @@ class Supervisor:
|
|
3704
5297
|
if name in self._process_groups:
|
3705
5298
|
return False
|
3706
5299
|
|
3707
|
-
group = self._process_groups[name] =
|
5300
|
+
group = self._process_groups[name] = self._process_group_factory(config)
|
3708
5301
|
group.after_setuid()
|
3709
5302
|
|
3710
|
-
|
5303
|
+
EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
|
3711
5304
|
return True
|
3712
5305
|
|
3713
5306
|
def remove_process_group(self, name: str) -> bool:
|
@@ -3718,7 +5311,7 @@ class Supervisor:
|
|
3718
5311
|
|
3719
5312
|
del self._process_groups[name]
|
3720
5313
|
|
3721
|
-
|
5314
|
+
EVENT_CALLBACKS.notify(ProcessGroupRemovedEvent(name))
|
3722
5315
|
return True
|
3723
5316
|
|
3724
5317
|
def get_process_map(self) -> ta.Dict[int, Dispatcher]:
|
@@ -3747,6 +5340,72 @@ class Supervisor:
|
|
3747
5340
|
|
3748
5341
|
return unstopped
|
3749
5342
|
|
5343
|
+
#
|
5344
|
+
|
5345
|
+
def main(self) -> None:
|
5346
|
+
self.setup()
|
5347
|
+
self.run()
|
5348
|
+
|
5349
|
+
@cached_nullary
|
5350
|
+
def setup(self) -> None:
|
5351
|
+
if not self._context.first:
|
5352
|
+
# prevent crash on libdispatch-based systems, at least for the first request
|
5353
|
+
self._context.cleanup_fds()
|
5354
|
+
|
5355
|
+
self._context.set_uid_or_exit()
|
5356
|
+
|
5357
|
+
if self._context.first:
|
5358
|
+
self._context.set_rlimits_or_exit()
|
5359
|
+
|
5360
|
+
# this sets the options.logger object delay logger instantiation until after setuid
|
5361
|
+
if not self._context.config.nocleanup:
|
5362
|
+
# clean up old automatic logs
|
5363
|
+
self._context.clear_auto_child_logdir()
|
5364
|
+
|
5365
|
+
def run(
|
5366
|
+
self,
|
5367
|
+
*,
|
5368
|
+
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
5369
|
+
) -> None:
|
5370
|
+
self._process_groups = {} # clear
|
5371
|
+
self._stop_groups = None # clear
|
5372
|
+
|
5373
|
+
EVENT_CALLBACKS.clear()
|
5374
|
+
|
5375
|
+
try:
|
5376
|
+
for config in self._context.config.groups or []:
|
5377
|
+
self.add_process_group(config)
|
5378
|
+
|
5379
|
+
self._context.set_signals()
|
5380
|
+
|
5381
|
+
if not self._context.config.nodaemon and self._context.first:
|
5382
|
+
self._context.daemonize()
|
5383
|
+
|
5384
|
+
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
5385
|
+
self._context.write_pidfile()
|
5386
|
+
|
5387
|
+
EVENT_CALLBACKS.notify(SupervisorRunningEvent())
|
5388
|
+
|
5389
|
+
while True:
|
5390
|
+
if callback is not None and not callback(self):
|
5391
|
+
break
|
5392
|
+
|
5393
|
+
self._run_once()
|
5394
|
+
|
5395
|
+
finally:
|
5396
|
+
self._context.cleanup()
|
5397
|
+
|
5398
|
+
#
|
5399
|
+
|
5400
|
+
def _run_once(self) -> None:
|
5401
|
+
self._poll()
|
5402
|
+
self._reap()
|
5403
|
+
self._handle_signal()
|
5404
|
+
self._tick()
|
5405
|
+
|
5406
|
+
if self._context.state < SupervisorStates.RUNNING:
|
5407
|
+
self._ordered_stop_groups_phase_2()
|
5408
|
+
|
3750
5409
|
def _ordered_stop_groups_phase_1(self) -> None:
|
3751
5410
|
if self._stop_groups:
|
3752
5411
|
# stop the last group (the one with the "highest" priority)
|
@@ -3763,7 +5422,7 @@ class Supervisor:
|
|
3763
5422
|
# down, so push it back on to the end of the stop group queue
|
3764
5423
|
self._stop_groups.append(group)
|
3765
5424
|
|
3766
|
-
def
|
5425
|
+
def _poll(self) -> None:
|
3767
5426
|
combined_map = {}
|
3768
5427
|
combined_map.update(self.get_process_map())
|
3769
5428
|
|
@@ -3775,7 +5434,7 @@ class Supervisor:
|
|
3775
5434
|
# first time, set the stopping flag, do a notification and set stop_groups
|
3776
5435
|
self._stopping = True
|
3777
5436
|
self._stop_groups = pgroups[:]
|
3778
|
-
|
5437
|
+
EVENT_CALLBACKS.notify(SupervisorStoppingEvent())
|
3779
5438
|
|
3780
5439
|
self._ordered_stop_groups_phase_1()
|
3781
5440
|
|
@@ -3835,33 +5494,6 @@ class Supervisor:
|
|
3835
5494
|
for group in pgroups:
|
3836
5495
|
group.transition()
|
3837
5496
|
|
3838
|
-
self._reap()
|
3839
|
-
self._handle_signal()
|
3840
|
-
self._tick()
|
3841
|
-
|
3842
|
-
if self._context.state < SupervisorStates.RUNNING:
|
3843
|
-
self._ordered_stop_groups_phase_2()
|
3844
|
-
|
3845
|
-
def _tick(self, now: ta.Optional[float] = None) -> None:
|
3846
|
-
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
3847
|
-
|
3848
|
-
if now is None:
|
3849
|
-
# now won't be None in unit tests
|
3850
|
-
now = time.time()
|
3851
|
-
|
3852
|
-
for event in TICK_EVENTS:
|
3853
|
-
period = event.period # type: ignore
|
3854
|
-
|
3855
|
-
last_tick = self._ticks.get(period)
|
3856
|
-
if last_tick is None:
|
3857
|
-
# we just started up
|
3858
|
-
last_tick = self._ticks[period] = timeslice(period, now)
|
3859
|
-
|
3860
|
-
this_tick = timeslice(period, now)
|
3861
|
-
if this_tick != last_tick:
|
3862
|
-
self._ticks[period] = this_tick
|
3863
|
-
notify_event(event(this_tick, self))
|
3864
|
-
|
3865
5497
|
def _reap(self, *, once: bool = False, depth: int = 0) -> None:
|
3866
5498
|
if depth >= 100:
|
3867
5499
|
return
|
@@ -3910,11 +5542,78 @@ class Supervisor:
|
|
3910
5542
|
else:
|
3911
5543
|
log.debug('received %s indicating nothing', signame(sig))
|
3912
5544
|
|
5545
|
+
def _tick(self, now: ta.Optional[float] = None) -> None:
|
5546
|
+
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
5547
|
+
|
5548
|
+
if now is None:
|
5549
|
+
# now won't be None in unit tests
|
5550
|
+
now = time.time()
|
5551
|
+
|
5552
|
+
for event in TICK_EVENTS:
|
5553
|
+
period = event.period # type: ignore
|
5554
|
+
|
5555
|
+
last_tick = self._ticks.get(period)
|
5556
|
+
if last_tick is None:
|
5557
|
+
# we just started up
|
5558
|
+
last_tick = self._ticks[period] = timeslice(period, now)
|
5559
|
+
|
5560
|
+
this_tick = timeslice(period, now)
|
5561
|
+
if this_tick != last_tick:
|
5562
|
+
self._ticks[period] = this_tick
|
5563
|
+
EVENT_CALLBACKS.notify(event(this_tick, self))
|
5564
|
+
|
3913
5565
|
|
3914
5566
|
########################################
|
3915
5567
|
# main.py
|
3916
5568
|
|
3917
5569
|
|
5570
|
+
##
|
5571
|
+
|
5572
|
+
|
5573
|
+
def build_server_bindings(
|
5574
|
+
config: ServerConfig,
|
5575
|
+
*,
|
5576
|
+
server_epoch: ta.Optional[ServerEpoch] = None,
|
5577
|
+
inherited_fds: ta.Optional[InheritedFds] = None,
|
5578
|
+
) -> InjectorBindings:
|
5579
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
5580
|
+
inj.bind(config),
|
5581
|
+
|
5582
|
+
inj.bind(ServerContext, singleton=True),
|
5583
|
+
inj.bind(AbstractServerContext, to_key=ServerContext),
|
5584
|
+
|
5585
|
+
inj.bind(Supervisor, singleton=True),
|
5586
|
+
]
|
5587
|
+
|
5588
|
+
#
|
5589
|
+
|
5590
|
+
def make_process_group_factory(injector: Injector) -> ProcessGroupFactory:
|
5591
|
+
def inner(group_config: ProcessGroupConfig) -> ProcessGroup:
|
5592
|
+
return injector.inject(functools.partial(ProcessGroup, group_config))
|
5593
|
+
return ProcessGroupFactory(inner)
|
5594
|
+
lst.append(inj.bind(make_process_group_factory))
|
5595
|
+
|
5596
|
+
def make_subprocess_factory(injector: Injector) -> SubprocessFactory:
|
5597
|
+
def inner(process_config: ProcessConfig, group: ProcessGroup) -> Subprocess:
|
5598
|
+
return injector.inject(functools.partial(Subprocess, process_config, group))
|
5599
|
+
return SubprocessFactory(inner)
|
5600
|
+
lst.append(inj.bind(make_subprocess_factory))
|
5601
|
+
|
5602
|
+
#
|
5603
|
+
|
5604
|
+
if server_epoch is not None:
|
5605
|
+
lst.append(inj.bind(server_epoch, key=ServerEpoch))
|
5606
|
+
if inherited_fds is not None:
|
5607
|
+
lst.append(inj.bind(inherited_fds, key=InheritedFds))
|
5608
|
+
|
5609
|
+
#
|
5610
|
+
|
5611
|
+
return inj.as_bindings(*lst)
|
5612
|
+
|
5613
|
+
|
5614
|
+
##
|
5615
|
+
|
5616
|
+
|
3918
5617
|
def main(
|
3919
5618
|
argv: ta.Optional[ta.Sequence[str]] = None,
|
3920
5619
|
*,
|
@@ -3925,6 +5624,7 @@ def main(
|
|
3925
5624
|
parser = argparse.ArgumentParser()
|
3926
5625
|
parser.add_argument('config_file', metavar='config-file')
|
3927
5626
|
parser.add_argument('--no-journald', action='store_true')
|
5627
|
+
parser.add_argument('--inherit-initial-fds', action='store_true')
|
3928
5628
|
args = parser.parse_args(argv)
|
3929
5629
|
|
3930
5630
|
#
|
@@ -3940,20 +5640,27 @@ def main(
|
|
3940
5640
|
|
3941
5641
|
#
|
3942
5642
|
|
5643
|
+
initial_fds: ta.Optional[InheritedFds] = None
|
5644
|
+
if args.inherit_initial_fds:
|
5645
|
+
initial_fds = InheritedFds(get_open_fds(0x10000))
|
5646
|
+
|
3943
5647
|
# if we hup, restart by making a new Supervisor()
|
3944
5648
|
for epoch in itertools.count():
|
3945
|
-
|
3946
|
-
|
3947
|
-
|
3948
|
-
|
3949
|
-
|
5649
|
+
config = read_config_file(
|
5650
|
+
os.path.expanduser(cf),
|
5651
|
+
ServerConfig,
|
5652
|
+
prepare=prepare_server_config,
|
5653
|
+
)
|
3950
5654
|
|
3951
|
-
|
5655
|
+
injector = inj.create_injector(build_server_bindings(
|
3952
5656
|
config,
|
3953
|
-
|
3954
|
-
|
5657
|
+
server_epoch=ServerEpoch(epoch),
|
5658
|
+
inherited_fds=initial_fds,
|
5659
|
+
))
|
5660
|
+
|
5661
|
+
context = injector.provide(ServerContext)
|
5662
|
+
supervisor = injector.provide(Supervisor)
|
3955
5663
|
|
3956
|
-
supervisor = Supervisor(context)
|
3957
5664
|
try:
|
3958
5665
|
supervisor.main()
|
3959
5666
|
except ExitNow:
|