ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev127__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.
- ominfra/clouds/aws/auth.py +1 -1
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +2 -2
- ominfra/scripts/supervisor.py +1796 -1218
- ominfra/supervisor/collections.py +52 -0
- ominfra/supervisor/context.py +2 -336
- ominfra/supervisor/datatypes.py +1 -63
- ominfra/supervisor/dispatchers.py +20 -324
- ominfra/supervisor/dispatchersimpl.py +342 -0
- ominfra/supervisor/groups.py +33 -111
- ominfra/supervisor/groupsimpl.py +86 -0
- ominfra/supervisor/inject.py +44 -19
- ominfra/supervisor/main.py +1 -1
- ominfra/supervisor/pipes.py +83 -0
- ominfra/supervisor/poller.py +6 -3
- ominfra/supervisor/privileges.py +65 -0
- ominfra/supervisor/processes.py +18 -0
- ominfra/supervisor/{process.py → processesimpl.py} +96 -330
- ominfra/supervisor/setup.py +38 -0
- ominfra/supervisor/setupimpl.py +261 -0
- ominfra/supervisor/signals.py +24 -16
- ominfra/supervisor/spawning.py +31 -0
- ominfra/supervisor/spawningimpl.py +347 -0
- ominfra/supervisor/supervisor.py +52 -77
- ominfra/supervisor/types.py +101 -45
- ominfra/supervisor/users.py +64 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -95,6 +95,10 @@ TomlParseFloat = ta.Callable[[str], ta.Any]
|
|
95
95
|
TomlKey = ta.Tuple[str, ...]
|
96
96
|
TomlPos = int # ta.TypeAlias
|
97
97
|
|
98
|
+
# ../collections.py
|
99
|
+
K = ta.TypeVar('K')
|
100
|
+
V = ta.TypeVar('V')
|
101
|
+
|
98
102
|
# ../../../omlish/lite/cached.py
|
99
103
|
T = ta.TypeVar('T')
|
100
104
|
|
@@ -102,6 +106,11 @@ T = ta.TypeVar('T')
|
|
102
106
|
SocketAddress = ta.Any
|
103
107
|
SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'SocketHandler']
|
104
108
|
|
109
|
+
# ../../../omlish/lite/typing.py
|
110
|
+
A0 = ta.TypeVar('A0')
|
111
|
+
A1 = ta.TypeVar('A1')
|
112
|
+
A2 = ta.TypeVar('A2')
|
113
|
+
|
105
114
|
# ../events.py
|
106
115
|
EventCallback = ta.Callable[['Event'], None]
|
107
116
|
|
@@ -109,6 +118,7 @@ EventCallback = ta.Callable[['Event'], None]
|
|
109
118
|
HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
|
110
119
|
|
111
120
|
# ../../../omlish/lite/inject.py
|
121
|
+
U = ta.TypeVar('U')
|
112
122
|
InjectorKeyCls = ta.Union[type, ta.NewType]
|
113
123
|
InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
|
114
124
|
InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
|
@@ -942,6 +952,55 @@ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
|
|
942
952
|
return safe_parse_float
|
943
953
|
|
944
954
|
|
955
|
+
########################################
|
956
|
+
# ../collections.py
|
957
|
+
|
958
|
+
|
959
|
+
class KeyedCollectionAccessors(abc.ABC, ta.Generic[K, V]):
|
960
|
+
@property
|
961
|
+
@abc.abstractmethod
|
962
|
+
def _by_key(self) -> ta.Mapping[K, V]:
|
963
|
+
raise NotImplementedError
|
964
|
+
|
965
|
+
def __iter__(self) -> ta.Iterator[V]:
|
966
|
+
return iter(self._by_key.values())
|
967
|
+
|
968
|
+
def __len__(self) -> int:
|
969
|
+
return len(self._by_key)
|
970
|
+
|
971
|
+
def __contains__(self, key: K) -> bool:
|
972
|
+
return key in self._by_key
|
973
|
+
|
974
|
+
def __getitem__(self, key: K) -> V:
|
975
|
+
return self._by_key[key]
|
976
|
+
|
977
|
+
def get(self, key: K, default: ta.Optional[V] = None) -> ta.Optional[V]:
|
978
|
+
return self._by_key.get(key, default)
|
979
|
+
|
980
|
+
def items(self) -> ta.Iterator[ta.Tuple[K, V]]:
|
981
|
+
return iter(self._by_key.items())
|
982
|
+
|
983
|
+
|
984
|
+
class KeyedCollection(KeyedCollectionAccessors[K, V]):
|
985
|
+
def __init__(self, items: ta.Iterable[V]) -> None:
|
986
|
+
super().__init__()
|
987
|
+
|
988
|
+
by_key: ta.Dict[K, V] = {}
|
989
|
+
for v in items:
|
990
|
+
if (k := self._key(v)) in by_key:
|
991
|
+
raise KeyError(f'key {k} of {v} already registered by {by_key[k]}')
|
992
|
+
by_key[k] = v
|
993
|
+
self.__by_key = by_key
|
994
|
+
|
995
|
+
@property
|
996
|
+
def _by_key(self) -> ta.Mapping[K, V]:
|
997
|
+
return self.__by_key
|
998
|
+
|
999
|
+
@abc.abstractmethod
|
1000
|
+
def _key(self, v: V) -> K:
|
1001
|
+
raise NotImplementedError
|
1002
|
+
|
1003
|
+
|
945
1004
|
########################################
|
946
1005
|
# ../datatypes.py
|
947
1006
|
|
@@ -975,43 +1034,7 @@ def logfile_name(val):
|
|
975
1034
|
return existing_dirpath(val)
|
976
1035
|
|
977
1036
|
|
978
|
-
|
979
|
-
try:
|
980
|
-
uid = int(name)
|
981
|
-
except ValueError:
|
982
|
-
try:
|
983
|
-
pwdrec = pwd.getpwnam(name)
|
984
|
-
except KeyError:
|
985
|
-
raise ValueError(f'Invalid user name {name}') # noqa
|
986
|
-
uid = pwdrec[2]
|
987
|
-
else:
|
988
|
-
try:
|
989
|
-
pwd.getpwuid(uid) # check if uid is valid
|
990
|
-
except KeyError:
|
991
|
-
raise ValueError(f'Invalid user id {name}') # noqa
|
992
|
-
return uid
|
993
|
-
|
994
|
-
|
995
|
-
def name_to_gid(name: str) -> int:
|
996
|
-
try:
|
997
|
-
gid = int(name)
|
998
|
-
except ValueError:
|
999
|
-
try:
|
1000
|
-
grprec = grp.getgrnam(name)
|
1001
|
-
except KeyError:
|
1002
|
-
raise ValueError(f'Invalid group name {name}') # noqa
|
1003
|
-
gid = grprec[2]
|
1004
|
-
else:
|
1005
|
-
try:
|
1006
|
-
grp.getgrgid(gid) # check if gid is valid
|
1007
|
-
except KeyError:
|
1008
|
-
raise ValueError(f'Invalid group id {name}') # noqa
|
1009
|
-
return gid
|
1010
|
-
|
1011
|
-
|
1012
|
-
def gid_for_uid(uid: int) -> int:
|
1013
|
-
pwrec = pwd.getpwuid(uid)
|
1014
|
-
return pwrec[3]
|
1037
|
+
##
|
1015
1038
|
|
1016
1039
|
|
1017
1040
|
def octal_type(arg: ta.Union[str, int]) -> int:
|
@@ -1083,29 +1106,6 @@ byte_size = SuffixMultiplier({
|
|
1083
1106
|
})
|
1084
1107
|
|
1085
1108
|
|
1086
|
-
# all valid signal numbers
|
1087
|
-
SIGNUMS = [getattr(signal, k) for k in dir(signal) if k.startswith('SIG')]
|
1088
|
-
|
1089
|
-
|
1090
|
-
def signal_number(value: ta.Union[int, str]) -> int:
|
1091
|
-
try:
|
1092
|
-
num = int(value)
|
1093
|
-
|
1094
|
-
except (ValueError, TypeError):
|
1095
|
-
name = value.strip().upper() # type: ignore
|
1096
|
-
if not name.startswith('SIG'):
|
1097
|
-
name = f'SIG{name}'
|
1098
|
-
|
1099
|
-
num = getattr(signal, name, None) # type: ignore
|
1100
|
-
if num is None:
|
1101
|
-
raise ValueError(f'value {value!r} is not a valid signal name') # noqa
|
1102
|
-
|
1103
|
-
if num not in SIGNUMS:
|
1104
|
-
raise ValueError(f'value {value!r} is not a valid signal number')
|
1105
|
-
|
1106
|
-
return num
|
1107
|
-
|
1108
|
-
|
1109
1109
|
class RestartWhenExitUnexpected:
|
1110
1110
|
pass
|
1111
1111
|
|
@@ -1144,6 +1144,70 @@ class NoPermissionError(ProcessError):
|
|
1144
1144
|
"""
|
1145
1145
|
|
1146
1146
|
|
1147
|
+
########################################
|
1148
|
+
# ../privileges.py
|
1149
|
+
|
1150
|
+
|
1151
|
+
def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
|
1152
|
+
"""
|
1153
|
+
Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
|
1154
|
+
and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
|
1155
|
+
dropped.
|
1156
|
+
"""
|
1157
|
+
|
1158
|
+
if user is None:
|
1159
|
+
return 'No user specified to setuid to!'
|
1160
|
+
|
1161
|
+
# get uid for user, which can be a number or username
|
1162
|
+
try:
|
1163
|
+
uid = int(user)
|
1164
|
+
except ValueError:
|
1165
|
+
try:
|
1166
|
+
pwrec = pwd.getpwnam(user) # type: ignore
|
1167
|
+
except KeyError:
|
1168
|
+
return f"Can't find username {user!r}"
|
1169
|
+
uid = pwrec[2]
|
1170
|
+
else:
|
1171
|
+
try:
|
1172
|
+
pwrec = pwd.getpwuid(uid)
|
1173
|
+
except KeyError:
|
1174
|
+
return f"Can't find uid {uid!r}"
|
1175
|
+
|
1176
|
+
current_uid = os.getuid()
|
1177
|
+
|
1178
|
+
if current_uid == uid:
|
1179
|
+
# do nothing and return successfully if the uid is already the current one. this allows a supervisord
|
1180
|
+
# running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
|
1181
|
+
# it.
|
1182
|
+
return None
|
1183
|
+
|
1184
|
+
if current_uid != 0:
|
1185
|
+
return "Can't drop privilege as nonroot user"
|
1186
|
+
|
1187
|
+
gid = pwrec[3]
|
1188
|
+
if hasattr(os, 'setgroups'):
|
1189
|
+
user = pwrec[0]
|
1190
|
+
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
1191
|
+
|
1192
|
+
# always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
|
1193
|
+
# group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
|
1194
|
+
# python 2.7 - this will be safe though for all unix /python version combos)
|
1195
|
+
groups.insert(0, gid)
|
1196
|
+
try:
|
1197
|
+
os.setgroups(groups)
|
1198
|
+
except OSError:
|
1199
|
+
return 'Could not set groups of effective user'
|
1200
|
+
|
1201
|
+
try:
|
1202
|
+
os.setgid(gid)
|
1203
|
+
except OSError:
|
1204
|
+
return 'Could not set group id of effective user'
|
1205
|
+
|
1206
|
+
os.setuid(uid)
|
1207
|
+
|
1208
|
+
return None
|
1209
|
+
|
1210
|
+
|
1147
1211
|
########################################
|
1148
1212
|
# ../signals.py
|
1149
1213
|
|
@@ -1151,25 +1215,33 @@ class NoPermissionError(ProcessError):
|
|
1151
1215
|
##
|
1152
1216
|
|
1153
1217
|
|
1154
|
-
|
1218
|
+
_SIGS_BY_NUM: ta.Mapping[int, signal.Signals] = {s.value: s for s in signal.Signals}
|
1219
|
+
_SIGS_BY_NAME: ta.Mapping[str, signal.Signals] = {s.name: s for s in signal.Signals}
|
1155
1220
|
|
1156
1221
|
|
1157
|
-
def
|
1158
|
-
|
1159
|
-
|
1160
|
-
_SIG_NAMES = _init_sig_names()
|
1161
|
-
return _SIG_NAMES.get(sig) or 'signal %d' % sig
|
1222
|
+
def sig_num(value: ta.Union[int, str]) -> int:
|
1223
|
+
try:
|
1224
|
+
num = int(value)
|
1162
1225
|
|
1226
|
+
except (ValueError, TypeError):
|
1227
|
+
name = value.strip().upper() # type: ignore
|
1228
|
+
if not name.startswith('SIG'):
|
1229
|
+
name = f'SIG{name}'
|
1230
|
+
|
1231
|
+
if (sn := _SIGS_BY_NAME.get(name)) is None:
|
1232
|
+
raise ValueError(f'value {value!r} is not a valid signal name') # noqa
|
1233
|
+
num = sn
|
1234
|
+
|
1235
|
+
if num not in _SIGS_BY_NUM:
|
1236
|
+
raise ValueError(f'value {value!r} is not a valid signal number')
|
1237
|
+
|
1238
|
+
return num
|
1163
1239
|
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
continue
|
1170
|
-
if k_startswith('SIG') and not k_startswith('SIG_'):
|
1171
|
-
d[v] = k
|
1172
|
-
return d
|
1240
|
+
|
1241
|
+
def sig_name(num: int) -> str:
|
1242
|
+
if (sig := _SIGS_BY_NUM.get(num)) is not None:
|
1243
|
+
return sig.name
|
1244
|
+
return f'signal {sig}'
|
1173
1245
|
|
1174
1246
|
|
1175
1247
|
##
|
@@ -1181,7 +1253,7 @@ class SignalReceiver:
|
|
1181
1253
|
|
1182
1254
|
self._signals_recvd: ta.List[int] = []
|
1183
1255
|
|
1184
|
-
def receive(self, sig: int, frame: ta.Any) -> None:
|
1256
|
+
def receive(self, sig: int, frame: ta.Any = None) -> None:
|
1185
1257
|
if sig not in self._signals_recvd:
|
1186
1258
|
self._signals_recvd.append(sig)
|
1187
1259
|
|
@@ -1257,6 +1329,70 @@ class SupervisorState(enum.IntEnum):
|
|
1257
1329
|
SHUTDOWN = -1
|
1258
1330
|
|
1259
1331
|
|
1332
|
+
########################################
|
1333
|
+
# ../users.py
|
1334
|
+
|
1335
|
+
|
1336
|
+
##
|
1337
|
+
|
1338
|
+
|
1339
|
+
def name_to_uid(name: str) -> int:
|
1340
|
+
try:
|
1341
|
+
uid = int(name)
|
1342
|
+
except ValueError:
|
1343
|
+
try:
|
1344
|
+
pwdrec = pwd.getpwnam(name)
|
1345
|
+
except KeyError:
|
1346
|
+
raise ValueError(f'Invalid user name {name}') # noqa
|
1347
|
+
uid = pwdrec[2]
|
1348
|
+
else:
|
1349
|
+
try:
|
1350
|
+
pwd.getpwuid(uid) # check if uid is valid
|
1351
|
+
except KeyError:
|
1352
|
+
raise ValueError(f'Invalid user id {name}') # noqa
|
1353
|
+
return uid
|
1354
|
+
|
1355
|
+
|
1356
|
+
def name_to_gid(name: str) -> int:
|
1357
|
+
try:
|
1358
|
+
gid = int(name)
|
1359
|
+
except ValueError:
|
1360
|
+
try:
|
1361
|
+
grprec = grp.getgrnam(name)
|
1362
|
+
except KeyError:
|
1363
|
+
raise ValueError(f'Invalid group name {name}') # noqa
|
1364
|
+
gid = grprec[2]
|
1365
|
+
else:
|
1366
|
+
try:
|
1367
|
+
grp.getgrgid(gid) # check if gid is valid
|
1368
|
+
except KeyError:
|
1369
|
+
raise ValueError(f'Invalid group id {name}') # noqa
|
1370
|
+
return gid
|
1371
|
+
|
1372
|
+
|
1373
|
+
def gid_for_uid(uid: int) -> int:
|
1374
|
+
pwrec = pwd.getpwuid(uid)
|
1375
|
+
return pwrec[3]
|
1376
|
+
|
1377
|
+
|
1378
|
+
##
|
1379
|
+
|
1380
|
+
|
1381
|
+
@dc.dataclass(frozen=True)
|
1382
|
+
class User:
|
1383
|
+
name: str
|
1384
|
+
uid: int
|
1385
|
+
gid: int
|
1386
|
+
|
1387
|
+
|
1388
|
+
def get_user(name: str) -> User:
|
1389
|
+
return User(
|
1390
|
+
name=name,
|
1391
|
+
uid=(uid := name_to_uid(name)),
|
1392
|
+
gid=gid_for_uid(uid),
|
1393
|
+
)
|
1394
|
+
|
1395
|
+
|
1260
1396
|
########################################
|
1261
1397
|
# ../../../omlish/lite/cached.py
|
1262
1398
|
|
@@ -1566,14 +1702,50 @@ class SocketHandler(abc.ABC):
|
|
1566
1702
|
# ../../../omlish/lite/typing.py
|
1567
1703
|
|
1568
1704
|
|
1705
|
+
##
|
1706
|
+
# A workaround for typing deficiencies (like `Argument 2 to NewType(...) must be subclassable`).
|
1707
|
+
|
1708
|
+
|
1569
1709
|
@dc.dataclass(frozen=True)
|
1570
|
-
class
|
1710
|
+
class AnyFunc(ta.Generic[T]):
|
1571
1711
|
fn: ta.Callable[..., T]
|
1572
1712
|
|
1573
1713
|
def __call__(self, *args: ta.Any, **kwargs: ta.Any) -> T:
|
1574
1714
|
return self.fn(*args, **kwargs)
|
1575
1715
|
|
1576
1716
|
|
1717
|
+
@dc.dataclass(frozen=True)
|
1718
|
+
class Func0(ta.Generic[T]):
|
1719
|
+
fn: ta.Callable[[], T]
|
1720
|
+
|
1721
|
+
def __call__(self) -> T:
|
1722
|
+
return self.fn()
|
1723
|
+
|
1724
|
+
|
1725
|
+
@dc.dataclass(frozen=True)
|
1726
|
+
class Func1(ta.Generic[A0, T]):
|
1727
|
+
fn: ta.Callable[[A0], T]
|
1728
|
+
|
1729
|
+
def __call__(self, a0: A0) -> T:
|
1730
|
+
return self.fn(a0)
|
1731
|
+
|
1732
|
+
|
1733
|
+
@dc.dataclass(frozen=True)
|
1734
|
+
class Func2(ta.Generic[A0, A1, T]):
|
1735
|
+
fn: ta.Callable[[A0, A1], T]
|
1736
|
+
|
1737
|
+
def __call__(self, a0: A0, a1: A1) -> T:
|
1738
|
+
return self.fn(a0, a1)
|
1739
|
+
|
1740
|
+
|
1741
|
+
@dc.dataclass(frozen=True)
|
1742
|
+
class Func3(ta.Generic[A0, A1, A2, T]):
|
1743
|
+
fn: ta.Callable[[A0, A1, A2], T]
|
1744
|
+
|
1745
|
+
def __call__(self, a0: A0, a1: A1, a2: A2) -> T:
|
1746
|
+
return self.fn(a0, a1, a2)
|
1747
|
+
|
1748
|
+
|
1577
1749
|
########################################
|
1578
1750
|
# ../events.py
|
1579
1751
|
|
@@ -1860,37 +2032,74 @@ def get_event_name_by_type(requested):
|
|
1860
2032
|
|
1861
2033
|
|
1862
2034
|
########################################
|
1863
|
-
# ../
|
2035
|
+
# ../setup.py
|
1864
2036
|
|
1865
2037
|
|
1866
2038
|
##
|
1867
2039
|
|
1868
2040
|
|
1869
|
-
|
1870
|
-
if isinstance(s, bytes):
|
1871
|
-
return s
|
1872
|
-
else:
|
1873
|
-
return s.encode(encoding)
|
2041
|
+
SupervisorUser = ta.NewType('SupervisorUser', User)
|
1874
2042
|
|
1875
2043
|
|
1876
|
-
|
1877
|
-
if isinstance(s, str):
|
1878
|
-
return s
|
1879
|
-
else:
|
1880
|
-
return s.decode(encoding)
|
2044
|
+
##
|
1881
2045
|
|
1882
2046
|
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
l -= 1
|
1887
|
-
return l
|
2047
|
+
class DaemonizeListener(abc.ABC): # noqa
|
2048
|
+
def before_daemonize(self) -> None: # noqa
|
2049
|
+
pass
|
1888
2050
|
|
2051
|
+
def after_daemonize(self) -> None: # noqa
|
2052
|
+
pass
|
1889
2053
|
|
1890
|
-
##
|
1891
2054
|
|
2055
|
+
DaemonizeListeners = ta.NewType('DaemonizeListeners', ta.Sequence[DaemonizeListener])
|
1892
2056
|
|
1893
|
-
|
2057
|
+
|
2058
|
+
##
|
2059
|
+
|
2060
|
+
|
2061
|
+
class SupervisorSetup(abc.ABC):
|
2062
|
+
@abc.abstractmethod
|
2063
|
+
def setup(self) -> None:
|
2064
|
+
raise NotImplementedError
|
2065
|
+
|
2066
|
+
@abc.abstractmethod
|
2067
|
+
def cleanup(self) -> None:
|
2068
|
+
raise NotImplementedError
|
2069
|
+
|
2070
|
+
|
2071
|
+
########################################
|
2072
|
+
# ../utils.py
|
2073
|
+
|
2074
|
+
|
2075
|
+
##
|
2076
|
+
|
2077
|
+
|
2078
|
+
def as_bytes(s: ta.Union[str, bytes], encoding: str = 'utf8') -> bytes:
|
2079
|
+
if isinstance(s, bytes):
|
2080
|
+
return s
|
2081
|
+
else:
|
2082
|
+
return s.encode(encoding)
|
2083
|
+
|
2084
|
+
|
2085
|
+
def as_string(s: ta.Union[str, bytes], encoding: str = 'utf8') -> str:
|
2086
|
+
if isinstance(s, str):
|
2087
|
+
return s
|
2088
|
+
else:
|
2089
|
+
return s.decode(encoding)
|
2090
|
+
|
2091
|
+
|
2092
|
+
def find_prefix_at_end(haystack: bytes, needle: bytes) -> int:
|
2093
|
+
l = len(needle) - 1
|
2094
|
+
while l and not haystack.endswith(needle[:l]):
|
2095
|
+
l -= 1
|
2096
|
+
return l
|
2097
|
+
|
2098
|
+
|
2099
|
+
##
|
2100
|
+
|
2101
|
+
|
2102
|
+
def compact_traceback() -> ta.Tuple[
|
1894
2103
|
ta.Tuple[str, str, int],
|
1895
2104
|
ta.Type[BaseException],
|
1896
2105
|
BaseException,
|
@@ -2059,6 +2268,41 @@ def timeslice(period: int, when: float) -> int:
|
|
2059
2268
|
|
2060
2269
|
########################################
|
2061
2270
|
# ../../../omlish/lite/http/parsing.py
|
2271
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
2272
|
+
# --------------------------------------------
|
2273
|
+
#
|
2274
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
2275
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
2276
|
+
# documentation.
|
2277
|
+
#
|
2278
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
2279
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
2280
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
2281
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
2282
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
|
2283
|
+
# alone or in any derivative version prepared by Licensee.
|
2284
|
+
#
|
2285
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
2286
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
2287
|
+
# any such work a brief summary of the changes made to Python.
|
2288
|
+
#
|
2289
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
2290
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
2291
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
2292
|
+
# RIGHTS.
|
2293
|
+
#
|
2294
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
2295
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
2296
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
2297
|
+
#
|
2298
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
2299
|
+
#
|
2300
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
2301
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
2302
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
2303
|
+
#
|
2304
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
2305
|
+
# License Agreement.
|
2062
2306
|
|
2063
2307
|
|
2064
2308
|
##
|
@@ -2489,11 +2733,23 @@ class Injector(abc.ABC):
|
|
2489
2733
|
raise NotImplementedError
|
2490
2734
|
|
2491
2735
|
@abc.abstractmethod
|
2492
|
-
def provide_kwargs(
|
2736
|
+
def provide_kwargs(
|
2737
|
+
self,
|
2738
|
+
obj: ta.Any,
|
2739
|
+
*,
|
2740
|
+
skip_args: int = 0,
|
2741
|
+
skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
|
2742
|
+
) -> ta.Mapping[str, ta.Any]:
|
2493
2743
|
raise NotImplementedError
|
2494
2744
|
|
2495
2745
|
@abc.abstractmethod
|
2496
|
-
def inject(
|
2746
|
+
def inject(
|
2747
|
+
self,
|
2748
|
+
obj: ta.Any,
|
2749
|
+
*,
|
2750
|
+
args: ta.Optional[ta.Sequence[ta.Any]] = None,
|
2751
|
+
kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
2752
|
+
) -> ta.Any:
|
2497
2753
|
raise NotImplementedError
|
2498
2754
|
|
2499
2755
|
def __getitem__(
|
@@ -2507,8 +2763,12 @@ class Injector(abc.ABC):
|
|
2507
2763
|
# exceptions
|
2508
2764
|
|
2509
2765
|
|
2766
|
+
class InjectorError(Exception):
|
2767
|
+
pass
|
2768
|
+
|
2769
|
+
|
2510
2770
|
@dc.dataclass(frozen=True)
|
2511
|
-
class InjectorKeyError(
|
2771
|
+
class InjectorKeyError(InjectorError):
|
2512
2772
|
key: InjectorKey
|
2513
2773
|
|
2514
2774
|
source: ta.Any = None
|
@@ -2715,29 +2975,49 @@ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey,
|
|
2715
2975
|
# inspection
|
2716
2976
|
|
2717
2977
|
|
2718
|
-
# inspect.signature(eval_str=True) was added in 3.10 and we have to support 3.8, so we have to get_type_hints to eval
|
2719
|
-
# str annotations *in addition to* getting the signature for parameter information.
|
2720
2978
|
class _InjectionInspection(ta.NamedTuple):
|
2721
2979
|
signature: inspect.Signature
|
2722
2980
|
type_hints: ta.Mapping[str, ta.Any]
|
2981
|
+
args_offset: int
|
2723
2982
|
|
2724
2983
|
|
2725
2984
|
_INJECTION_INSPECTION_CACHE: ta.MutableMapping[ta.Any, _InjectionInspection] = weakref.WeakKeyDictionary()
|
2726
2985
|
|
2727
2986
|
|
2728
2987
|
def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
|
2729
|
-
|
2988
|
+
tgt = obj
|
2989
|
+
if isinstance(tgt, type) and tgt.__init__ is not object.__init__: # type: ignore[misc]
|
2990
|
+
# Python 3.8's inspect.signature can't handle subclasses overriding __new__, always generating *args/**kwargs.
|
2991
|
+
# - https://bugs.python.org/issue40897
|
2992
|
+
# - https://github.com/python/cpython/commit/df7c62980d15acd3125dfbd81546dad359f7add7
|
2993
|
+
tgt = tgt.__init__ # type: ignore[misc]
|
2994
|
+
has_generic_base = True
|
2995
|
+
else:
|
2996
|
+
has_generic_base = False
|
2997
|
+
|
2998
|
+
# inspect.signature(eval_str=True) was added in 3.10 and we have to support 3.8, so we have to get_type_hints to
|
2999
|
+
# eval str annotations *in addition to* getting the signature for parameter information.
|
3000
|
+
uw = tgt
|
3001
|
+
has_partial = False
|
2730
3002
|
while True:
|
2731
3003
|
if isinstance(uw, functools.partial):
|
3004
|
+
has_partial = True
|
2732
3005
|
uw = uw.func
|
2733
3006
|
else:
|
2734
3007
|
if (uw2 := inspect.unwrap(uw)) is uw:
|
2735
3008
|
break
|
2736
3009
|
uw = uw2
|
2737
3010
|
|
3011
|
+
if has_generic_base and has_partial:
|
3012
|
+
raise InjectorError(
|
3013
|
+
'Injector inspection does not currently support both a typing.Generic base and a functools.partial: '
|
3014
|
+
f'{obj}',
|
3015
|
+
)
|
3016
|
+
|
2738
3017
|
return _InjectionInspection(
|
2739
|
-
inspect.signature(
|
3018
|
+
inspect.signature(tgt),
|
2740
3019
|
ta.get_type_hints(uw),
|
3020
|
+
1 if has_generic_base else 0,
|
2741
3021
|
)
|
2742
3022
|
|
2743
3023
|
|
@@ -2768,14 +3048,23 @@ def build_injection_kwargs_target(
|
|
2768
3048
|
obj: ta.Any,
|
2769
3049
|
*,
|
2770
3050
|
skip_args: int = 0,
|
2771
|
-
skip_kwargs: ta.Optional[ta.Iterable[
|
3051
|
+
skip_kwargs: ta.Optional[ta.Iterable[str]] = None,
|
2772
3052
|
raw_optional: bool = False,
|
2773
3053
|
) -> InjectionKwargsTarget:
|
2774
3054
|
insp = _injection_inspect(obj)
|
2775
3055
|
|
2776
|
-
|
3056
|
+
params = list(insp.signature.parameters.values())
|
3057
|
+
|
3058
|
+
skip_names: ta.Set[str] = set()
|
3059
|
+
if skip_kwargs is not None:
|
3060
|
+
skip_names.update(check_not_isinstance(skip_kwargs, str))
|
3061
|
+
|
3062
|
+
seen: ta.Set[InjectorKey] = set()
|
2777
3063
|
kws: ta.List[InjectionKwarg] = []
|
2778
|
-
for p in
|
3064
|
+
for p in params[insp.args_offset + skip_args:]:
|
3065
|
+
if p.name in skip_names:
|
3066
|
+
continue
|
3067
|
+
|
2779
3068
|
if p.annotation is inspect.Signature.empty:
|
2780
3069
|
if p.default is not inspect.Parameter.empty:
|
2781
3070
|
raise KeyError(f'{obj}, {p.name}')
|
@@ -2784,6 +3073,7 @@ def build_injection_kwargs_target(
|
|
2784
3073
|
if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
|
2785
3074
|
raise TypeError(insp)
|
2786
3075
|
|
3076
|
+
# 3.8 inspect.signature doesn't eval_str but typing.get_type_hints does, so prefer that.
|
2787
3077
|
ann = insp.type_hints.get(p.name, p.annotation)
|
2788
3078
|
if (
|
2789
3079
|
not raw_optional and
|
@@ -2851,8 +3141,19 @@ class _Injector(Injector):
|
|
2851
3141
|
return v.must()
|
2852
3142
|
raise UnboundInjectorKeyError(key)
|
2853
3143
|
|
2854
|
-
def provide_kwargs(
|
2855
|
-
|
3144
|
+
def provide_kwargs(
|
3145
|
+
self,
|
3146
|
+
obj: ta.Any,
|
3147
|
+
*,
|
3148
|
+
skip_args: int = 0,
|
3149
|
+
skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
|
3150
|
+
) -> ta.Mapping[str, ta.Any]:
|
3151
|
+
kt = build_injection_kwargs_target(
|
3152
|
+
obj,
|
3153
|
+
skip_args=skip_args,
|
3154
|
+
skip_kwargs=skip_kwargs,
|
3155
|
+
)
|
3156
|
+
|
2856
3157
|
ret: ta.Dict[str, ta.Any] = {}
|
2857
3158
|
for kw in kt.kwargs:
|
2858
3159
|
if kw.has_default:
|
@@ -2864,9 +3165,24 @@ class _Injector(Injector):
|
|
2864
3165
|
ret[kw.name] = v
|
2865
3166
|
return ret
|
2866
3167
|
|
2867
|
-
def inject(
|
2868
|
-
|
2869
|
-
|
3168
|
+
def inject(
|
3169
|
+
self,
|
3170
|
+
obj: ta.Any,
|
3171
|
+
*,
|
3172
|
+
args: ta.Optional[ta.Sequence[ta.Any]] = None,
|
3173
|
+
kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
3174
|
+
) -> ta.Any:
|
3175
|
+
provided = self.provide_kwargs(
|
3176
|
+
obj,
|
3177
|
+
skip_args=len(args) if args is not None else 0,
|
3178
|
+
skip_kwargs=kwargs if kwargs is not None else None,
|
3179
|
+
)
|
3180
|
+
|
3181
|
+
return obj(
|
3182
|
+
*(args if args is not None else ()),
|
3183
|
+
**(kwargs if kwargs is not None else {}),
|
3184
|
+
**provided,
|
3185
|
+
)
|
2870
3186
|
|
2871
3187
|
|
2872
3188
|
###
|
@@ -3005,16 +3321,42 @@ class InjectorBinder:
|
|
3005
3321
|
|
3006
3322
|
|
3007
3323
|
def make_injector_factory(
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3324
|
+
fn: ta.Callable[..., T],
|
3325
|
+
cls: U,
|
3326
|
+
ann: ta.Any = None,
|
3327
|
+
) -> ta.Callable[..., U]:
|
3328
|
+
if ann is None:
|
3329
|
+
ann = cls
|
3330
|
+
|
3331
|
+
def outer(injector: Injector) -> ann:
|
3012
3332
|
def inner(*args, **kwargs):
|
3013
|
-
return injector.inject(
|
3014
|
-
return
|
3333
|
+
return injector.inject(fn, args=args, kwargs=kwargs)
|
3334
|
+
return cls(inner) # type: ignore
|
3335
|
+
|
3015
3336
|
return outer
|
3016
3337
|
|
3017
3338
|
|
3339
|
+
def make_injector_array_type(
|
3340
|
+
ele: ta.Union[InjectorKey, InjectorKeyCls],
|
3341
|
+
cls: U,
|
3342
|
+
ann: ta.Any = None,
|
3343
|
+
) -> ta.Callable[..., U]:
|
3344
|
+
if isinstance(ele, InjectorKey):
|
3345
|
+
if not ele.array:
|
3346
|
+
raise InjectorError('Provided key must be array', ele)
|
3347
|
+
key = ele
|
3348
|
+
else:
|
3349
|
+
key = dc.replace(as_injector_key(ele), array=True)
|
3350
|
+
|
3351
|
+
if ann is None:
|
3352
|
+
ann = cls
|
3353
|
+
|
3354
|
+
def inner(injector: Injector) -> ann:
|
3355
|
+
return cls(injector.provide(key)) # type: ignore[operator]
|
3356
|
+
|
3357
|
+
return inner
|
3358
|
+
|
3359
|
+
|
3018
3360
|
##
|
3019
3361
|
|
3020
3362
|
|
@@ -3049,8 +3391,8 @@ class Injection:
|
|
3049
3391
|
# injector
|
3050
3392
|
|
3051
3393
|
@classmethod
|
3052
|
-
def create_injector(cls, *args: InjectorBindingOrBindings,
|
3053
|
-
return _Injector(as_injector_bindings(*args),
|
3394
|
+
def create_injector(cls, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
|
3395
|
+
return _Injector(as_injector_bindings(*args), parent)
|
3054
3396
|
|
3055
3397
|
# binder
|
3056
3398
|
|
@@ -3090,10 +3432,20 @@ class Injection:
|
|
3090
3432
|
@classmethod
|
3091
3433
|
def bind_factory(
|
3092
3434
|
cls,
|
3093
|
-
|
3094
|
-
|
3435
|
+
fn: ta.Callable[..., T],
|
3436
|
+
cls_: U,
|
3437
|
+
ann: ta.Any = None,
|
3438
|
+
) -> InjectorBindingOrBindings:
|
3439
|
+
return cls.bind(make_injector_factory(fn, cls_, ann))
|
3440
|
+
|
3441
|
+
@classmethod
|
3442
|
+
def bind_array_type(
|
3443
|
+
cls,
|
3444
|
+
ele: ta.Union[InjectorKey, InjectorKeyCls],
|
3445
|
+
cls_: U,
|
3446
|
+
ann: ta.Any = None,
|
3095
3447
|
) -> InjectorBindingOrBindings:
|
3096
|
-
return cls.bind(
|
3448
|
+
return cls.bind(make_injector_array_type(ele, cls_, ann))
|
3097
3449
|
|
3098
3450
|
|
3099
3451
|
inj = Injection
|
@@ -3344,7 +3696,7 @@ class StandardLogFormatter(logging.Formatter):
|
|
3344
3696
|
return ct.strftime(datefmt) # noqa
|
3345
3697
|
else:
|
3346
3698
|
t = ct.strftime('%Y-%m-%d %H:%M:%S')
|
3347
|
-
return '%s.%03d' % (t, record.msecs)
|
3699
|
+
return '%s.%03d' % (t, record.msecs) # noqa
|
3348
3700
|
|
3349
3701
|
|
3350
3702
|
##
|
@@ -3930,11 +4282,91 @@ def build_config_named_children(
|
|
3930
4282
|
return lst
|
3931
4283
|
|
3932
4284
|
|
4285
|
+
########################################
|
4286
|
+
# ../pipes.py
|
4287
|
+
|
4288
|
+
|
4289
|
+
@dc.dataclass(frozen=True)
|
4290
|
+
class ProcessPipes:
|
4291
|
+
child_stdin: ta.Optional[int] = None
|
4292
|
+
stdin: ta.Optional[int] = None
|
4293
|
+
|
4294
|
+
stdout: ta.Optional[int] = None
|
4295
|
+
child_stdout: ta.Optional[int] = None
|
4296
|
+
|
4297
|
+
stderr: ta.Optional[int] = None
|
4298
|
+
child_stderr: ta.Optional[int] = None
|
4299
|
+
|
4300
|
+
def child_fds(self) -> ta.List[int]:
|
4301
|
+
return [fd for fd in [self.child_stdin, self.child_stdout, self.child_stderr] if fd is not None]
|
4302
|
+
|
4303
|
+
def parent_fds(self) -> ta.List[int]:
|
4304
|
+
return [fd for fd in [self.stdin, self.stdout, self.stderr] if fd is not None]
|
4305
|
+
|
4306
|
+
|
4307
|
+
def make_process_pipes(stderr=True) -> ProcessPipes:
|
4308
|
+
"""
|
4309
|
+
Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
|
4310
|
+
read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
|
4311
|
+
"""
|
4312
|
+
|
4313
|
+
pipes: ta.Dict[str, ta.Optional[int]] = {
|
4314
|
+
'child_stdin': None,
|
4315
|
+
'stdin': None,
|
4316
|
+
|
4317
|
+
'stdout': None,
|
4318
|
+
'child_stdout': None,
|
4319
|
+
|
4320
|
+
'stderr': None,
|
4321
|
+
'child_stderr': None,
|
4322
|
+
}
|
4323
|
+
|
4324
|
+
try:
|
4325
|
+
pipes['child_stdin'], pipes['stdin'] = os.pipe()
|
4326
|
+
pipes['stdout'], pipes['child_stdout'] = os.pipe()
|
4327
|
+
|
4328
|
+
if stderr:
|
4329
|
+
pipes['stderr'], pipes['child_stderr'] = os.pipe()
|
4330
|
+
|
4331
|
+
for fd in (
|
4332
|
+
pipes['stdout'],
|
4333
|
+
pipes['stderr'],
|
4334
|
+
pipes['stdin'],
|
4335
|
+
):
|
4336
|
+
if fd is not None:
|
4337
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
|
4338
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
4339
|
+
|
4340
|
+
return ProcessPipes(**pipes)
|
4341
|
+
|
4342
|
+
except OSError:
|
4343
|
+
for fd in pipes.values():
|
4344
|
+
if fd is not None:
|
4345
|
+
close_fd(fd)
|
4346
|
+
|
4347
|
+
raise
|
4348
|
+
|
4349
|
+
|
4350
|
+
def close_pipes(pipes: ProcessPipes) -> None:
|
4351
|
+
close_parent_pipes(pipes)
|
4352
|
+
close_child_pipes(pipes)
|
4353
|
+
|
4354
|
+
|
4355
|
+
def close_parent_pipes(pipes: ProcessPipes) -> None:
|
4356
|
+
for fd in pipes.parent_fds():
|
4357
|
+
close_fd(fd)
|
4358
|
+
|
4359
|
+
|
4360
|
+
def close_child_pipes(pipes: ProcessPipes) -> None:
|
4361
|
+
for fd in pipes.child_fds():
|
4362
|
+
close_fd(fd)
|
4363
|
+
|
4364
|
+
|
3933
4365
|
########################################
|
3934
4366
|
# ../poller.py
|
3935
4367
|
|
3936
4368
|
|
3937
|
-
class Poller(abc.ABC):
|
4369
|
+
class Poller(DaemonizeListener, abc.ABC):
|
3938
4370
|
def __init__(self) -> None:
|
3939
4371
|
super().__init__()
|
3940
4372
|
|
@@ -4152,8 +4584,9 @@ else:
|
|
4152
4584
|
|
4153
4585
|
def get_poller_impl() -> ta.Type[Poller]:
|
4154
4586
|
if (
|
4155
|
-
sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
|
4156
|
-
hasattr(select, 'kqueue') and
|
4587
|
+
(sys.platform == 'darwin' or sys.platform.startswith('freebsd')) and
|
4588
|
+
hasattr(select, 'kqueue') and
|
4589
|
+
KqueuePoller is not None
|
4157
4590
|
):
|
4158
4591
|
return KqueuePoller
|
4159
4592
|
elif hasattr(select, 'poll'):
|
@@ -4877,6 +5310,32 @@ class CoroHttpServerSocketHandler(SocketHandler):
|
|
4877
5310
|
# ../types.py
|
4878
5311
|
|
4879
5312
|
|
5313
|
+
##
|
5314
|
+
|
5315
|
+
|
5316
|
+
ServerEpoch = ta.NewType('ServerEpoch', int)
|
5317
|
+
|
5318
|
+
|
5319
|
+
##
|
5320
|
+
|
5321
|
+
|
5322
|
+
@functools.total_ordering
|
5323
|
+
class ConfigPriorityOrdered(abc.ABC):
|
5324
|
+
@property
|
5325
|
+
@abc.abstractmethod
|
5326
|
+
def config(self) -> ta.Any:
|
5327
|
+
raise NotImplementedError
|
5328
|
+
|
5329
|
+
def __lt__(self, other):
|
5330
|
+
return self.config.priority < other.config.priority
|
5331
|
+
|
5332
|
+
def __eq__(self, other):
|
5333
|
+
return self.config.priority == other.config.priority
|
5334
|
+
|
5335
|
+
|
5336
|
+
##
|
5337
|
+
|
5338
|
+
|
4880
5339
|
class ServerContext(abc.ABC):
|
4881
5340
|
@property
|
4882
5341
|
@abc.abstractmethod
|
@@ -4898,23 +5357,23 @@ class ServerContext(abc.ABC):
|
|
4898
5357
|
raise NotImplementedError
|
4899
5358
|
|
4900
5359
|
|
5360
|
+
##
|
5361
|
+
|
5362
|
+
|
4901
5363
|
class Dispatcher(abc.ABC):
|
5364
|
+
@property
|
4902
5365
|
@abc.abstractmethod
|
4903
|
-
def
|
5366
|
+
def process(self) -> 'Process':
|
4904
5367
|
raise NotImplementedError
|
4905
5368
|
|
5369
|
+
@property
|
4906
5370
|
@abc.abstractmethod
|
4907
|
-
def
|
5371
|
+
def channel(self) -> str:
|
4908
5372
|
raise NotImplementedError
|
4909
5373
|
|
4910
|
-
|
4911
|
-
raise TypeError
|
4912
|
-
|
4913
|
-
def handle_write_event(self) -> None:
|
4914
|
-
raise TypeError
|
4915
|
-
|
5374
|
+
@property
|
4916
5375
|
@abc.abstractmethod
|
4917
|
-
def
|
5376
|
+
def fd(self) -> int:
|
4918
5377
|
raise NotImplementedError
|
4919
5378
|
|
4920
5379
|
@property
|
@@ -4922,48 +5381,36 @@ class Dispatcher(abc.ABC):
|
|
4922
5381
|
def closed(self) -> bool:
|
4923
5382
|
raise NotImplementedError
|
4924
5383
|
|
5384
|
+
#
|
4925
5385
|
|
4926
|
-
class OutputDispatcher(Dispatcher, abc.ABC):
|
4927
|
-
pass
|
4928
|
-
|
4929
|
-
|
4930
|
-
class InputDispatcher(Dispatcher, abc.ABC):
|
4931
5386
|
@abc.abstractmethod
|
4932
|
-
def
|
5387
|
+
def close(self) -> None:
|
4933
5388
|
raise NotImplementedError
|
4934
5389
|
|
4935
5390
|
@abc.abstractmethod
|
4936
|
-
def
|
5391
|
+
def handle_error(self) -> None:
|
4937
5392
|
raise NotImplementedError
|
4938
5393
|
|
5394
|
+
#
|
4939
5395
|
|
4940
|
-
@functools.total_ordering
|
4941
|
-
class Process(abc.ABC):
|
4942
|
-
@property
|
4943
5396
|
@abc.abstractmethod
|
4944
|
-
def
|
5397
|
+
def readable(self) -> bool:
|
4945
5398
|
raise NotImplementedError
|
4946
5399
|
|
4947
|
-
@property
|
4948
5400
|
@abc.abstractmethod
|
4949
|
-
def
|
5401
|
+
def writable(self) -> bool:
|
4950
5402
|
raise NotImplementedError
|
4951
5403
|
|
4952
|
-
|
4953
|
-
return self.config.priority < other.config.priority
|
5404
|
+
#
|
4954
5405
|
|
4955
|
-
def
|
4956
|
-
|
5406
|
+
def handle_read_event(self) -> None:
|
5407
|
+
raise TypeError
|
4957
5408
|
|
4958
|
-
|
4959
|
-
|
4960
|
-
def context(self) -> ServerContext:
|
4961
|
-
raise NotImplementedError
|
5409
|
+
def handle_write_event(self) -> None:
|
5410
|
+
raise TypeError
|
4962
5411
|
|
4963
|
-
@abc.abstractmethod
|
4964
|
-
def finish(self, sts: int) -> None:
|
4965
|
-
raise NotImplementedError
|
4966
5412
|
|
5413
|
+
class OutputDispatcher(Dispatcher, abc.ABC):
|
4967
5414
|
@abc.abstractmethod
|
4968
5415
|
def remove_logs(self) -> None:
|
4969
5416
|
raise NotImplementedError
|
@@ -4972,67 +5419,104 @@ class Process(abc.ABC):
|
|
4972
5419
|
def reopen_logs(self) -> None:
|
4973
5420
|
raise NotImplementedError
|
4974
5421
|
|
4975
|
-
@abc.abstractmethod
|
4976
|
-
def stop(self) -> ta.Optional[str]:
|
4977
|
-
raise NotImplementedError
|
4978
5422
|
|
5423
|
+
class InputDispatcher(Dispatcher, abc.ABC):
|
4979
5424
|
@abc.abstractmethod
|
4980
|
-
def
|
5425
|
+
def write(self, chars: ta.Union[bytes, str]) -> None:
|
4981
5426
|
raise NotImplementedError
|
4982
5427
|
|
4983
5428
|
@abc.abstractmethod
|
4984
|
-
def
|
5429
|
+
def flush(self) -> None:
|
4985
5430
|
raise NotImplementedError
|
4986
5431
|
|
4987
|
-
|
4988
|
-
|
5432
|
+
|
5433
|
+
##
|
5434
|
+
|
5435
|
+
|
5436
|
+
class Process(ConfigPriorityOrdered, abc.ABC):
|
5437
|
+
@property
|
5438
|
+
@abc.abstractmethod
|
5439
|
+
def name(self) -> str:
|
4989
5440
|
raise NotImplementedError
|
4990
5441
|
|
5442
|
+
@property
|
4991
5443
|
@abc.abstractmethod
|
4992
|
-
def
|
5444
|
+
def config(self) -> ProcessConfig:
|
4993
5445
|
raise NotImplementedError
|
4994
5446
|
|
5447
|
+
@property
|
4995
5448
|
@abc.abstractmethod
|
4996
|
-
def
|
5449
|
+
def group(self) -> 'ProcessGroup':
|
4997
5450
|
raise NotImplementedError
|
4998
5451
|
|
5452
|
+
@property
|
5453
|
+
@abc.abstractmethod
|
5454
|
+
def pid(self) -> int:
|
5455
|
+
raise NotImplementedError
|
5456
|
+
|
5457
|
+
#
|
4999
5458
|
|
5000
|
-
@functools.total_ordering
|
5001
|
-
class ProcessGroup(abc.ABC):
|
5002
5459
|
@property
|
5003
5460
|
@abc.abstractmethod
|
5004
|
-
def
|
5461
|
+
def context(self) -> ServerContext:
|
5005
5462
|
raise NotImplementedError
|
5006
5463
|
|
5007
|
-
|
5008
|
-
|
5464
|
+
@abc.abstractmethod
|
5465
|
+
def finish(self, sts: int) -> None:
|
5466
|
+
raise NotImplementedError
|
5009
5467
|
|
5010
|
-
|
5011
|
-
|
5468
|
+
@abc.abstractmethod
|
5469
|
+
def stop(self) -> ta.Optional[str]:
|
5470
|
+
raise NotImplementedError
|
5471
|
+
|
5472
|
+
@abc.abstractmethod
|
5473
|
+
def give_up(self) -> None:
|
5474
|
+
raise NotImplementedError
|
5012
5475
|
|
5013
5476
|
@abc.abstractmethod
|
5014
5477
|
def transition(self) -> None:
|
5015
5478
|
raise NotImplementedError
|
5016
5479
|
|
5017
5480
|
@abc.abstractmethod
|
5018
|
-
def
|
5481
|
+
def get_state(self) -> ProcessState:
|
5482
|
+
raise NotImplementedError
|
5483
|
+
|
5484
|
+
@abc.abstractmethod
|
5485
|
+
def after_setuid(self) -> None:
|
5019
5486
|
raise NotImplementedError
|
5020
5487
|
|
5488
|
+
@abc.abstractmethod
|
5489
|
+
def get_dispatchers(self) -> 'Dispatchers':
|
5490
|
+
raise NotImplementedError
|
5491
|
+
|
5492
|
+
|
5493
|
+
##
|
5494
|
+
|
5495
|
+
|
5496
|
+
class ProcessGroup(
|
5497
|
+
ConfigPriorityOrdered,
|
5498
|
+
KeyedCollectionAccessors[str, Process],
|
5499
|
+
abc.ABC,
|
5500
|
+
):
|
5021
5501
|
@property
|
5022
5502
|
@abc.abstractmethod
|
5023
5503
|
def name(self) -> str:
|
5024
5504
|
raise NotImplementedError
|
5025
5505
|
|
5506
|
+
@property
|
5026
5507
|
@abc.abstractmethod
|
5027
|
-
def
|
5508
|
+
def config(self) -> ProcessGroupConfig:
|
5028
5509
|
raise NotImplementedError
|
5029
5510
|
|
5511
|
+
@property
|
5030
5512
|
@abc.abstractmethod
|
5031
|
-
def
|
5513
|
+
def by_name(self) -> ta.Mapping[str, Process]:
|
5032
5514
|
raise NotImplementedError
|
5033
5515
|
|
5516
|
+
#
|
5517
|
+
|
5034
5518
|
@abc.abstractmethod
|
5035
|
-
def
|
5519
|
+
def stop_all(self) -> None:
|
5036
5520
|
raise NotImplementedError
|
5037
5521
|
|
5038
5522
|
@abc.abstractmethod
|
@@ -5040,7 +5524,7 @@ class ProcessGroup(abc.ABC):
|
|
5040
5524
|
raise NotImplementedError
|
5041
5525
|
|
5042
5526
|
@abc.abstractmethod
|
5043
|
-
def
|
5527
|
+
def before_remove(self) -> None:
|
5044
5528
|
raise NotImplementedError
|
5045
5529
|
|
5046
5530
|
|
@@ -5048,9 +5532,6 @@ class ProcessGroup(abc.ABC):
|
|
5048
5532
|
# ../context.py
|
5049
5533
|
|
5050
5534
|
|
5051
|
-
ServerEpoch = ta.NewType('ServerEpoch', int)
|
5052
|
-
|
5053
|
-
|
5054
5535
|
class ServerContextImpl(ServerContext):
|
5055
5536
|
def __init__(
|
5056
5537
|
self,
|
@@ -5068,16 +5549,6 @@ class ServerContextImpl(ServerContext):
|
|
5068
5549
|
self._pid_history: ta.Dict[int, Process] = {}
|
5069
5550
|
self._state: SupervisorState = SupervisorState.RUNNING
|
5070
5551
|
|
5071
|
-
if config.user is not None:
|
5072
|
-
uid = name_to_uid(config.user)
|
5073
|
-
self._uid: ta.Optional[int] = uid
|
5074
|
-
self._gid: ta.Optional[int] = gid_for_uid(uid)
|
5075
|
-
else:
|
5076
|
-
self._uid = None
|
5077
|
-
self._gid = None
|
5078
|
-
|
5079
|
-
self._unlink_pidfile = False
|
5080
|
-
|
5081
5552
|
@property
|
5082
5553
|
def config(self) -> ServerConfig:
|
5083
5554
|
return self._config
|
@@ -5101,15 +5572,7 @@ class ServerContextImpl(ServerContext):
|
|
5101
5572
|
def pid_history(self) -> ta.Dict[int, Process]:
|
5102
5573
|
return self._pid_history
|
5103
5574
|
|
5104
|
-
|
5105
|
-
def uid(self) -> ta.Optional[int]:
|
5106
|
-
return self._uid
|
5107
|
-
|
5108
|
-
@property
|
5109
|
-
def gid(self) -> ta.Optional[int]:
|
5110
|
-
return self._gid
|
5111
|
-
|
5112
|
-
##
|
5575
|
+
#
|
5113
5576
|
|
5114
5577
|
def waitpid(self) -> ta.Tuple[ta.Optional[int], ta.Optional[int]]:
|
5115
5578
|
# Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
|
@@ -5129,355 +5592,99 @@ class ServerContextImpl(ServerContext):
|
|
5129
5592
|
pid, sts = None, None
|
5130
5593
|
return pid, sts
|
5131
5594
|
|
5132
|
-
def
|
5133
|
-
|
5134
|
-
|
5135
|
-
|
5136
|
-
|
5595
|
+
def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
|
5596
|
+
prefix = f'{name}-{channel}---{identifier}-'
|
5597
|
+
logfile = mktempfile(
|
5598
|
+
suffix='.log',
|
5599
|
+
prefix=prefix,
|
5600
|
+
dir=self.config.child_logdir,
|
5601
|
+
)
|
5602
|
+
return logfile
|
5137
5603
|
|
5138
|
-
if self.uid is None:
|
5139
|
-
if os.getuid() == 0:
|
5140
|
-
warnings.warn(
|
5141
|
-
'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
|
5142
|
-
'config file. If you intend to run as root, you can set user=root in the config file to avoid '
|
5143
|
-
'this message.',
|
5144
|
-
)
|
5145
|
-
else:
|
5146
|
-
msg = drop_privileges(self.uid)
|
5147
|
-
if msg is None:
|
5148
|
-
log.info('Set uid to user %s succeeded', self.uid)
|
5149
|
-
else: # failed to drop privileges
|
5150
|
-
raise RuntimeError(msg)
|
5151
5604
|
|
5152
|
-
|
5153
|
-
|
5154
|
-
Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
|
5155
|
-
the process via usage() if any rlimits could not be set.
|
5156
|
-
"""
|
5605
|
+
########################################
|
5606
|
+
# ../dispatchers.py
|
5157
5607
|
|
5158
|
-
limits = []
|
5159
5608
|
|
5160
|
-
|
5161
|
-
|
5162
|
-
|
5163
|
-
'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
|
5164
|
-
'"minfds" command-line argument or config file setting. The current environment will only allow '
|
5165
|
-
'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
|
5166
|
-
'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
|
5167
|
-
'process to start.'
|
5168
|
-
),
|
5169
|
-
'min': self.config.minfds,
|
5170
|
-
'resource': resource.RLIMIT_NOFILE,
|
5171
|
-
'name': 'RLIMIT_NOFILE',
|
5172
|
-
})
|
5609
|
+
class Dispatchers(KeyedCollection[int, Dispatcher]):
|
5610
|
+
def _key(self, v: Dispatcher) -> int:
|
5611
|
+
return v.fd
|
5173
5612
|
|
5174
|
-
|
5175
|
-
limits.append({
|
5176
|
-
'msg': (
|
5177
|
-
'The minimum number of available processes required to run this program is %(min_limit)s as per '
|
5178
|
-
'the "minprocs" command-line argument or config file setting. The current environment will only '
|
5179
|
-
'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
|
5180
|
-
'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
|
5181
|
-
'program to start.'
|
5182
|
-
),
|
5183
|
-
'min': self.config.minprocs,
|
5184
|
-
'resource': resource.RLIMIT_NPROC,
|
5185
|
-
'name': 'RLIMIT_NPROC',
|
5186
|
-
})
|
5613
|
+
#
|
5187
5614
|
|
5188
|
-
|
5189
|
-
|
5190
|
-
|
5191
|
-
|
5192
|
-
|
5615
|
+
def drain(self) -> None:
|
5616
|
+
for d in self:
|
5617
|
+
# note that we *must* call readable() for every dispatcher, as it may have side effects for a given
|
5618
|
+
# dispatcher (eg. call handle_listener_state_change for event listener processes)
|
5619
|
+
if d.readable():
|
5620
|
+
d.handle_read_event()
|
5621
|
+
if d.writable():
|
5622
|
+
d.handle_write_event()
|
5193
5623
|
|
5194
|
-
|
5624
|
+
#
|
5195
5625
|
|
5196
|
-
|
5197
|
-
|
5198
|
-
|
5199
|
-
|
5200
|
-
# usage
|
5201
|
-
hard = min_limit # type: ignore
|
5626
|
+
def remove_logs(self) -> None:
|
5627
|
+
for d in self:
|
5628
|
+
if isinstance(d, OutputDispatcher):
|
5629
|
+
d.remove_logs()
|
5202
5630
|
|
5203
|
-
|
5204
|
-
|
5205
|
-
|
5206
|
-
|
5207
|
-
raise RuntimeError(msg % dict( # type: ignore # noqa
|
5208
|
-
min_limit=min_limit,
|
5209
|
-
res=res,
|
5210
|
-
name=name,
|
5211
|
-
soft=soft,
|
5212
|
-
hard=hard,
|
5213
|
-
))
|
5631
|
+
def reopen_logs(self) -> None:
|
5632
|
+
for d in self:
|
5633
|
+
if isinstance(d, OutputDispatcher):
|
5634
|
+
d.reopen_logs()
|
5214
5635
|
|
5215
|
-
def cleanup(self) -> None:
|
5216
|
-
if self._unlink_pidfile:
|
5217
|
-
try_unlink(self.config.pidfile)
|
5218
|
-
self._poller.close()
|
5219
5636
|
|
5220
|
-
|
5221
|
-
|
5222
|
-
start = 5
|
5223
|
-
os.closerange(start, self.config.minfds)
|
5637
|
+
########################################
|
5638
|
+
# ../dispatchersimpl.py
|
5224
5639
|
|
5225
|
-
def clear_auto_child_logdir(self) -> None:
|
5226
|
-
# must be called after realize()
|
5227
|
-
child_logdir = self.config.child_logdir
|
5228
|
-
fnre = re.compile(rf'.+?---{self.config.identifier}-\S+\.log\.?\d{{0,4}}')
|
5229
|
-
try:
|
5230
|
-
filenames = os.listdir(child_logdir)
|
5231
|
-
except OSError:
|
5232
|
-
log.warning('Could not clear child_log dir')
|
5233
|
-
return
|
5234
5640
|
|
5235
|
-
|
5236
|
-
|
5237
|
-
|
5238
|
-
|
5239
|
-
|
5240
|
-
|
5241
|
-
|
5641
|
+
class BaseDispatcherImpl(Dispatcher, abc.ABC):
|
5642
|
+
def __init__(
|
5643
|
+
self,
|
5644
|
+
process: Process,
|
5645
|
+
channel: str,
|
5646
|
+
fd: int,
|
5647
|
+
*,
|
5648
|
+
event_callbacks: EventCallbacks,
|
5649
|
+
) -> None:
|
5650
|
+
super().__init__()
|
5242
5651
|
|
5243
|
-
|
5244
|
-
self.
|
5245
|
-
self.
|
5246
|
-
self.
|
5652
|
+
self._process = process # process which "owns" this dispatcher
|
5653
|
+
self._channel = channel # 'stderr' or 'stdout'
|
5654
|
+
self._fd = fd
|
5655
|
+
self._event_callbacks = event_callbacks
|
5247
5656
|
|
5248
|
-
|
5249
|
-
# To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
|
5250
|
-
# our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
|
5251
|
-
# our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
|
5252
|
-
# terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
|
5253
|
-
# use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
|
5254
|
-
# session and process group and setting itself up as a new session leader.
|
5255
|
-
#
|
5256
|
-
# Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
|
5257
|
-
# of ourselves that is guaranteed to not be a session group leader.
|
5258
|
-
#
|
5259
|
-
# We also change directories, set stderr and stdout to null, and change our umask.
|
5260
|
-
#
|
5261
|
-
# This explanation was (gratefully) garnered from
|
5262
|
-
# http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
|
5657
|
+
self._closed = False # True if close() has been called
|
5263
5658
|
|
5264
|
-
|
5265
|
-
if pid != 0:
|
5266
|
-
# Parent
|
5267
|
-
log.debug('supervisord forked; parent exiting')
|
5268
|
-
real_exit(0)
|
5659
|
+
#
|
5269
5660
|
|
5270
|
-
|
5271
|
-
|
5272
|
-
if self.config.directory:
|
5273
|
-
try:
|
5274
|
-
os.chdir(self.config.directory)
|
5275
|
-
except OSError as err:
|
5276
|
-
log.critical("can't chdir into %r: %s", self.config.directory, err)
|
5277
|
-
else:
|
5278
|
-
log.info('set current directory: %r', self.config.directory)
|
5661
|
+
def __repr__(self) -> str:
|
5662
|
+
return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
|
5279
5663
|
|
5280
|
-
|
5281
|
-
os.dup2(1, os.open('/dev/null', os.O_WRONLY))
|
5282
|
-
os.dup2(2, os.open('/dev/null', os.O_WRONLY))
|
5664
|
+
#
|
5283
5665
|
|
5284
|
-
|
5666
|
+
@property
|
5667
|
+
def process(self) -> Process:
|
5668
|
+
return self._process
|
5285
5669
|
|
5286
|
-
|
5670
|
+
@property
|
5671
|
+
def channel(self) -> str:
|
5672
|
+
return self._channel
|
5287
5673
|
|
5288
|
-
|
5289
|
-
|
5290
|
-
|
5674
|
+
@property
|
5675
|
+
def fd(self) -> int:
|
5676
|
+
return self._fd
|
5291
5677
|
|
5292
|
-
|
5293
|
-
|
5294
|
-
|
5295
|
-
suffix='.log',
|
5296
|
-
prefix=prefix,
|
5297
|
-
dir=self.config.child_logdir,
|
5298
|
-
)
|
5299
|
-
return logfile
|
5678
|
+
@property
|
5679
|
+
def closed(self) -> bool:
|
5680
|
+
return self._closed
|
5300
5681
|
|
5301
|
-
|
5302
|
-
pid = os.getpid()
|
5303
|
-
try:
|
5304
|
-
with open(self.config.pidfile, 'w') as f:
|
5305
|
-
f.write(f'{pid}\n')
|
5306
|
-
except OSError:
|
5307
|
-
log.critical('could not write pidfile %s', self.config.pidfile)
|
5308
|
-
else:
|
5309
|
-
self._unlink_pidfile = True
|
5310
|
-
log.info('supervisord started with pid %s', pid)
|
5682
|
+
#
|
5311
5683
|
|
5312
|
-
|
5313
|
-
|
5314
|
-
|
5315
|
-
|
5316
|
-
and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
|
5317
|
-
dropped.
|
5318
|
-
"""
|
5319
|
-
|
5320
|
-
if user is None:
|
5321
|
-
return 'No user specified to setuid to!'
|
5322
|
-
|
5323
|
-
# get uid for user, which can be a number or username
|
5324
|
-
try:
|
5325
|
-
uid = int(user)
|
5326
|
-
except ValueError:
|
5327
|
-
try:
|
5328
|
-
pwrec = pwd.getpwnam(user) # type: ignore
|
5329
|
-
except KeyError:
|
5330
|
-
return f"Can't find username {user!r}"
|
5331
|
-
uid = pwrec[2]
|
5332
|
-
else:
|
5333
|
-
try:
|
5334
|
-
pwrec = pwd.getpwuid(uid)
|
5335
|
-
except KeyError:
|
5336
|
-
return f"Can't find uid {uid!r}"
|
5337
|
-
|
5338
|
-
current_uid = os.getuid()
|
5339
|
-
|
5340
|
-
if current_uid == uid:
|
5341
|
-
# do nothing and return successfully if the uid is already the current one. this allows a supervisord
|
5342
|
-
# running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
|
5343
|
-
# it.
|
5344
|
-
return None
|
5345
|
-
|
5346
|
-
if current_uid != 0:
|
5347
|
-
return "Can't drop privilege as nonroot user"
|
5348
|
-
|
5349
|
-
gid = pwrec[3]
|
5350
|
-
if hasattr(os, 'setgroups'):
|
5351
|
-
user = pwrec[0]
|
5352
|
-
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
5353
|
-
|
5354
|
-
# always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
|
5355
|
-
# group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
|
5356
|
-
# python 2.7 - this will be safe though for all unix /python version combos)
|
5357
|
-
groups.insert(0, gid)
|
5358
|
-
try:
|
5359
|
-
os.setgroups(groups)
|
5360
|
-
except OSError:
|
5361
|
-
return 'Could not set groups of effective user'
|
5362
|
-
|
5363
|
-
try:
|
5364
|
-
os.setgid(gid)
|
5365
|
-
except OSError:
|
5366
|
-
return 'Could not set group id of effective user'
|
5367
|
-
|
5368
|
-
os.setuid(uid)
|
5369
|
-
|
5370
|
-
return None
|
5371
|
-
|
5372
|
-
|
5373
|
-
def make_pipes(stderr=True) -> ta.Mapping[str, int]:
|
5374
|
-
"""
|
5375
|
-
Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
|
5376
|
-
read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
|
5377
|
-
"""
|
5378
|
-
|
5379
|
-
pipes: ta.Dict[str, ta.Optional[int]] = {
|
5380
|
-
'child_stdin': None,
|
5381
|
-
'stdin': None,
|
5382
|
-
'stdout': None,
|
5383
|
-
'child_stdout': None,
|
5384
|
-
'stderr': None,
|
5385
|
-
'child_stderr': None,
|
5386
|
-
}
|
5387
|
-
|
5388
|
-
try:
|
5389
|
-
stdin, child_stdin = os.pipe()
|
5390
|
-
pipes['child_stdin'], pipes['stdin'] = stdin, child_stdin
|
5391
|
-
|
5392
|
-
stdout, child_stdout = os.pipe()
|
5393
|
-
pipes['stdout'], pipes['child_stdout'] = stdout, child_stdout
|
5394
|
-
|
5395
|
-
if stderr:
|
5396
|
-
stderr, child_stderr = os.pipe()
|
5397
|
-
pipes['stderr'], pipes['child_stderr'] = stderr, child_stderr
|
5398
|
-
|
5399
|
-
for fd in (pipes['stdout'], pipes['stderr'], pipes['stdin']):
|
5400
|
-
if fd is not None:
|
5401
|
-
flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
|
5402
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
5403
|
-
|
5404
|
-
return pipes # type: ignore
|
5405
|
-
|
5406
|
-
except OSError:
|
5407
|
-
for fd in pipes.values():
|
5408
|
-
if fd is not None:
|
5409
|
-
close_fd(fd)
|
5410
|
-
raise
|
5411
|
-
|
5412
|
-
|
5413
|
-
def close_parent_pipes(pipes: ta.Mapping[str, int]) -> None:
|
5414
|
-
for fdname in ('stdin', 'stdout', 'stderr'):
|
5415
|
-
fd = pipes.get(fdname)
|
5416
|
-
if fd is not None:
|
5417
|
-
close_fd(fd)
|
5418
|
-
|
5419
|
-
|
5420
|
-
def close_child_pipes(pipes: ta.Mapping[str, int]) -> None:
|
5421
|
-
for fdname in ('child_stdin', 'child_stdout', 'child_stderr'):
|
5422
|
-
fd = pipes.get(fdname)
|
5423
|
-
if fd is not None:
|
5424
|
-
close_fd(fd)
|
5425
|
-
|
5426
|
-
|
5427
|
-
def check_execv_args(filename, argv, st) -> None:
|
5428
|
-
if st is None:
|
5429
|
-
raise NotFoundError(f"can't find command {filename!r}")
|
5430
|
-
|
5431
|
-
elif stat.S_ISDIR(st[stat.ST_MODE]):
|
5432
|
-
raise NotExecutableError(f'command at {filename!r} is a directory')
|
5433
|
-
|
5434
|
-
elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
|
5435
|
-
raise NotExecutableError(f'command at {filename!r} is not executable')
|
5436
|
-
|
5437
|
-
elif not os.access(filename, os.X_OK):
|
5438
|
-
raise NoPermissionError(f'no permission to run command {filename!r}')
|
5439
|
-
|
5440
|
-
|
5441
|
-
########################################
|
5442
|
-
# ../dispatchers.py
|
5443
|
-
|
5444
|
-
|
5445
|
-
class BaseDispatcherImpl(Dispatcher, abc.ABC):
|
5446
|
-
def __init__(
|
5447
|
-
self,
|
5448
|
-
process: Process,
|
5449
|
-
channel: str,
|
5450
|
-
fd: int,
|
5451
|
-
*,
|
5452
|
-
event_callbacks: EventCallbacks,
|
5453
|
-
) -> None:
|
5454
|
-
super().__init__()
|
5455
|
-
|
5456
|
-
self._process = process # process which "owns" this dispatcher
|
5457
|
-
self._channel = channel # 'stderr' or 'stdout'
|
5458
|
-
self._fd = fd
|
5459
|
-
self._event_callbacks = event_callbacks
|
5460
|
-
|
5461
|
-
self._closed = False # True if close() has been called
|
5462
|
-
|
5463
|
-
def __repr__(self) -> str:
|
5464
|
-
return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
|
5465
|
-
|
5466
|
-
@property
|
5467
|
-
def process(self) -> Process:
|
5468
|
-
return self._process
|
5469
|
-
|
5470
|
-
@property
|
5471
|
-
def channel(self) -> str:
|
5472
|
-
return self._channel
|
5473
|
-
|
5474
|
-
@property
|
5475
|
-
def fd(self) -> int:
|
5476
|
-
return self._fd
|
5477
|
-
|
5478
|
-
@property
|
5479
|
-
def closed(self) -> bool:
|
5480
|
-
return self._closed
|
5684
|
+
def close(self) -> None:
|
5685
|
+
if not self._closed:
|
5686
|
+
log.debug('fd %s closed, stopped monitoring %s', self._fd, self)
|
5687
|
+
self._closed = True
|
5481
5688
|
|
5482
5689
|
def handle_error(self) -> None:
|
5483
5690
|
nil, t, v, tbinfo = compact_traceback()
|
@@ -5485,11 +5692,6 @@ class BaseDispatcherImpl(Dispatcher, abc.ABC):
|
|
5485
5692
|
log.critical('uncaptured python exception, closing channel %s (%s:%s %s)', repr(self), t, v, tbinfo)
|
5486
5693
|
self.close()
|
5487
5694
|
|
5488
|
-
def close(self) -> None:
|
5489
|
-
if not self._closed:
|
5490
|
-
log.debug('fd %s closed, stopped monitoring %s', self._fd, self)
|
5491
|
-
self._closed = True
|
5492
|
-
|
5493
5695
|
|
5494
5696
|
class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
|
5495
5697
|
"""
|
@@ -5759,58 +5961,133 @@ class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
|
|
5759
5961
|
# ../groups.py
|
5760
5962
|
|
5761
5963
|
|
5762
|
-
|
5964
|
+
class ProcessGroupManager(KeyedCollectionAccessors[str, ProcessGroup]):
|
5965
|
+
def __init__(
|
5966
|
+
self,
|
5967
|
+
*,
|
5968
|
+
event_callbacks: EventCallbacks,
|
5969
|
+
) -> None:
|
5970
|
+
super().__init__()
|
5971
|
+
|
5972
|
+
self._event_callbacks = event_callbacks
|
5973
|
+
|
5974
|
+
self._by_name: ta.Dict[str, ProcessGroup] = {}
|
5975
|
+
|
5976
|
+
@property
|
5977
|
+
def _by_key(self) -> ta.Mapping[str, ProcessGroup]:
|
5978
|
+
return self._by_name
|
5979
|
+
|
5980
|
+
#
|
5981
|
+
|
5982
|
+
def all_processes(self) -> ta.Iterator[Process]:
|
5983
|
+
for g in self:
|
5984
|
+
yield from g
|
5985
|
+
|
5986
|
+
#
|
5987
|
+
|
5988
|
+
def add(self, group: ProcessGroup) -> None:
|
5989
|
+
if (name := group.name) in self._by_name:
|
5990
|
+
raise KeyError(f'Process group already exists: {name}')
|
5991
|
+
|
5992
|
+
self._by_name[name] = group
|
5993
|
+
|
5994
|
+
self._event_callbacks.notify(ProcessGroupAddedEvent(name))
|
5995
|
+
|
5996
|
+
def remove(self, name: str) -> None:
|
5997
|
+
group = self._by_name[name]
|
5998
|
+
|
5999
|
+
group.before_remove()
|
6000
|
+
|
6001
|
+
del self._by_name[name]
|
6002
|
+
|
6003
|
+
self._event_callbacks.notify(ProcessGroupRemovedEvent(name))
|
6004
|
+
|
6005
|
+
def clear(self) -> None:
|
6006
|
+
# FIXME: events?
|
6007
|
+
self._by_name.clear()
|
6008
|
+
|
6009
|
+
#
|
6010
|
+
|
6011
|
+
class Diff(ta.NamedTuple):
|
6012
|
+
added: ta.List[ProcessGroupConfig]
|
6013
|
+
changed: ta.List[ProcessGroupConfig]
|
6014
|
+
removed: ta.List[ProcessGroupConfig]
|
6015
|
+
|
6016
|
+
def diff(self, new: ta.Sequence[ProcessGroupConfig]) -> Diff:
|
6017
|
+
cur = [group.config for group in self]
|
5763
6018
|
|
6019
|
+
cur_by_name = {cfg.name: cfg for cfg in cur}
|
6020
|
+
new_by_name = {cfg.name: cfg for cfg in new}
|
5764
6021
|
|
5765
|
-
|
6022
|
+
added = [cand for cand in new if cand.name not in cur_by_name]
|
6023
|
+
removed = [cand for cand in cur if cand.name not in new_by_name]
|
6024
|
+
changed = [cand for cand in new if cand != cur_by_name.get(cand.name, cand)]
|
6025
|
+
|
6026
|
+
return ProcessGroupManager.Diff(
|
6027
|
+
added,
|
6028
|
+
changed,
|
6029
|
+
removed,
|
6030
|
+
)
|
6031
|
+
|
6032
|
+
|
6033
|
+
########################################
|
6034
|
+
# ../groupsimpl.py
|
6035
|
+
|
6036
|
+
|
6037
|
+
class ProcessFactory(Func2[ProcessConfig, ProcessGroup, Process]):
|
6038
|
+
pass
|
5766
6039
|
|
5767
6040
|
|
5768
6041
|
class ProcessGroupImpl(ProcessGroup):
|
5769
6042
|
def __init__(
|
5770
6043
|
self,
|
5771
6044
|
config: ProcessGroupConfig,
|
5772
|
-
context: ServerContext,
|
5773
6045
|
*,
|
5774
6046
|
process_factory: ProcessFactory,
|
5775
6047
|
):
|
5776
6048
|
super().__init__()
|
5777
6049
|
|
5778
6050
|
self._config = config
|
5779
|
-
self._context = context
|
5780
6051
|
self._process_factory = process_factory
|
5781
6052
|
|
5782
|
-
|
6053
|
+
by_name: ta.Dict[str, Process] = {}
|
5783
6054
|
for pconfig in self._config.processes or []:
|
5784
|
-
|
5785
|
-
|
6055
|
+
p = check_isinstance(self._process_factory(pconfig, self), Process)
|
6056
|
+
if p.name in by_name:
|
6057
|
+
raise KeyError(f'name {p.name} of process {p} already registered by {by_name[p.name]}')
|
6058
|
+
by_name[pconfig.name] = p
|
6059
|
+
self._by_name = by_name
|
5786
6060
|
|
5787
6061
|
@property
|
5788
|
-
def
|
5789
|
-
return self.
|
6062
|
+
def _by_key(self) -> ta.Mapping[str, Process]:
|
6063
|
+
return self._by_name
|
6064
|
+
|
6065
|
+
#
|
6066
|
+
|
6067
|
+
def __repr__(self) -> str:
|
6068
|
+
return f'<{self.__class__.__name__} instance at {id(self)} named {self._config.name}>'
|
6069
|
+
|
6070
|
+
#
|
5790
6071
|
|
5791
6072
|
@property
|
5792
6073
|
def name(self) -> str:
|
5793
6074
|
return self._config.name
|
5794
6075
|
|
5795
6076
|
@property
|
5796
|
-
def
|
5797
|
-
return self.
|
6077
|
+
def config(self) -> ProcessGroupConfig:
|
6078
|
+
return self._config
|
5798
6079
|
|
5799
|
-
|
5800
|
-
|
5801
|
-
|
5802
|
-
return f'<{self.__class__.__name__} instance at {id(self)} named {name}>'
|
6080
|
+
@property
|
6081
|
+
def by_name(self) -> ta.Mapping[str, Process]:
|
6082
|
+
return self._by_name
|
5803
6083
|
|
5804
|
-
|
5805
|
-
for process in self._processes.values():
|
5806
|
-
process.remove_logs()
|
6084
|
+
#
|
5807
6085
|
|
5808
|
-
def
|
5809
|
-
for
|
5810
|
-
process.reopen_logs()
|
6086
|
+
def get_unstopped_processes(self) -> ta.List[Process]:
|
6087
|
+
return [x for x in self if not x.get_state().stopped]
|
5811
6088
|
|
5812
6089
|
def stop_all(self) -> None:
|
5813
|
-
processes = list(self.
|
6090
|
+
processes = list(self._by_name.values())
|
5814
6091
|
processes.sort()
|
5815
6092
|
processes.reverse() # stop in desc priority order
|
5816
6093
|
|
@@ -5828,90 +6105,642 @@ class ProcessGroupImpl(ProcessGroup):
|
|
5828
6105
|
# BACKOFF -> FATAL
|
5829
6106
|
proc.give_up()
|
5830
6107
|
|
5831
|
-
def get_unstopped_processes(self) -> ta.List[Process]:
|
5832
|
-
return [x for x in self._processes.values() if not x.get_state().stopped]
|
5833
|
-
|
5834
|
-
def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
|
5835
|
-
dispatchers: dict = {}
|
5836
|
-
for process in self._processes.values():
|
5837
|
-
dispatchers.update(process.get_dispatchers())
|
5838
|
-
return dispatchers
|
5839
|
-
|
5840
6108
|
def before_remove(self) -> None:
|
5841
6109
|
pass
|
5842
6110
|
|
5843
|
-
def transition(self) -> None:
|
5844
|
-
for proc in self._processes.values():
|
5845
|
-
proc.transition()
|
5846
6111
|
|
5847
|
-
|
5848
|
-
|
5849
|
-
proc.create_auto_child_logs()
|
6112
|
+
########################################
|
6113
|
+
# ../processes.py
|
5850
6114
|
|
5851
6115
|
|
5852
6116
|
##
|
5853
6117
|
|
5854
6118
|
|
5855
|
-
class
|
5856
|
-
|
5857
|
-
self,
|
5858
|
-
*,
|
5859
|
-
event_callbacks: EventCallbacks,
|
5860
|
-
) -> None:
|
5861
|
-
super().__init__()
|
6119
|
+
class ProcessStateError(RuntimeError):
|
6120
|
+
pass
|
5862
6121
|
|
5863
|
-
self._event_callbacks = event_callbacks
|
5864
6122
|
|
5865
|
-
|
6123
|
+
##
|
5866
6124
|
|
5867
|
-
def get(self, name: str) -> ta.Optional[ProcessGroup]:
|
5868
|
-
return self._by_name.get(name)
|
5869
6125
|
|
5870
|
-
|
5871
|
-
|
6126
|
+
class PidHistory(ta.Dict[int, Process]):
|
6127
|
+
pass
|
5872
6128
|
|
5873
|
-
def __len__(self) -> int:
|
5874
|
-
return len(self._by_name)
|
5875
6129
|
|
5876
|
-
|
5877
|
-
|
6130
|
+
########################################
|
6131
|
+
# ../setupimpl.py
|
5878
6132
|
|
5879
|
-
def all(self) -> ta.Mapping[str, ProcessGroup]:
|
5880
|
-
return self._by_name
|
5881
6133
|
|
5882
|
-
|
5883
|
-
if (name := group.name) in self._by_name:
|
5884
|
-
raise KeyError(f'Process group already exists: {name}')
|
6134
|
+
##
|
5885
6135
|
|
5886
|
-
self._by_name[name] = group
|
5887
6136
|
|
5888
|
-
|
6137
|
+
class SupervisorSetupImpl(SupervisorSetup):
|
6138
|
+
def __init__(
|
6139
|
+
self,
|
6140
|
+
*,
|
6141
|
+
config: ServerConfig,
|
6142
|
+
user: ta.Optional[SupervisorUser] = None,
|
6143
|
+
epoch: ServerEpoch = ServerEpoch(0),
|
6144
|
+
daemonize_listeners: DaemonizeListeners = DaemonizeListeners([]),
|
6145
|
+
) -> None:
|
6146
|
+
super().__init__()
|
5889
6147
|
|
5890
|
-
|
5891
|
-
|
6148
|
+
self._config = config
|
6149
|
+
self._user = user
|
6150
|
+
self._epoch = epoch
|
6151
|
+
self._daemonize_listeners = daemonize_listeners
|
6152
|
+
|
6153
|
+
#
|
6154
|
+
|
6155
|
+
@property
|
6156
|
+
def first(self) -> bool:
|
6157
|
+
return not self._epoch
|
6158
|
+
|
6159
|
+
#
|
6160
|
+
|
6161
|
+
@cached_nullary
|
6162
|
+
def setup(self) -> None:
|
6163
|
+
if not self.first:
|
6164
|
+
# prevent crash on libdispatch-based systems, at least for the first request
|
6165
|
+
self._cleanup_fds()
|
6166
|
+
|
6167
|
+
self._set_uid_or_exit()
|
6168
|
+
|
6169
|
+
if self.first:
|
6170
|
+
self._set_rlimits_or_exit()
|
6171
|
+
|
6172
|
+
# this sets the options.logger object delay logger instantiation until after setuid
|
6173
|
+
if not self._config.nocleanup:
|
6174
|
+
# clean up old automatic logs
|
6175
|
+
self._clear_auto_child_logdir()
|
6176
|
+
|
6177
|
+
if not self._config.nodaemon and self.first:
|
6178
|
+
self._daemonize()
|
6179
|
+
|
6180
|
+
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
6181
|
+
self._write_pidfile()
|
6182
|
+
|
6183
|
+
@cached_nullary
|
6184
|
+
def cleanup(self) -> None:
|
6185
|
+
self._cleanup_pidfile()
|
6186
|
+
|
6187
|
+
#
|
6188
|
+
|
6189
|
+
def _cleanup_fds(self) -> None:
|
6190
|
+
# try to close any leaked file descriptors (for reload)
|
6191
|
+
start = 5
|
6192
|
+
os.closerange(start, self._config.minfds)
|
6193
|
+
|
6194
|
+
#
|
6195
|
+
|
6196
|
+
def _set_uid_or_exit(self) -> None:
|
6197
|
+
"""
|
6198
|
+
Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
|
6199
|
+
process via usage() if privileges could not be dropped.
|
6200
|
+
"""
|
6201
|
+
|
6202
|
+
if self._user is None:
|
6203
|
+
if os.getuid() == 0:
|
6204
|
+
warnings.warn(
|
6205
|
+
'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
|
6206
|
+
'config file. If you intend to run as root, you can set user=root in the config file to avoid '
|
6207
|
+
'this message.',
|
6208
|
+
)
|
6209
|
+
else:
|
6210
|
+
msg = drop_privileges(self._user.uid)
|
6211
|
+
if msg is None:
|
6212
|
+
log.info('Set uid to user %s succeeded', self._user.uid)
|
6213
|
+
else: # failed to drop privileges
|
6214
|
+
raise RuntimeError(msg)
|
6215
|
+
|
6216
|
+
#
|
6217
|
+
|
6218
|
+
def _set_rlimits_or_exit(self) -> None:
|
6219
|
+
"""
|
6220
|
+
Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
|
6221
|
+
the process via usage() if any rlimits could not be set.
|
6222
|
+
"""
|
6223
|
+
|
6224
|
+
limits = []
|
6225
|
+
|
6226
|
+
if hasattr(resource, 'RLIMIT_NOFILE'):
|
6227
|
+
limits.append({
|
6228
|
+
'msg': (
|
6229
|
+
'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
|
6230
|
+
'"minfds" command-line argument or config file setting. The current environment will only allow '
|
6231
|
+
'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
|
6232
|
+
'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
|
6233
|
+
'process to start.'
|
6234
|
+
),
|
6235
|
+
'min': self._config.minfds,
|
6236
|
+
'resource': resource.RLIMIT_NOFILE,
|
6237
|
+
'name': 'RLIMIT_NOFILE',
|
6238
|
+
})
|
6239
|
+
|
6240
|
+
if hasattr(resource, 'RLIMIT_NPROC'):
|
6241
|
+
limits.append({
|
6242
|
+
'msg': (
|
6243
|
+
'The minimum number of available processes required to run this program is %(min_limit)s as per '
|
6244
|
+
'the "minprocs" command-line argument or config file setting. The current environment will only '
|
6245
|
+
'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
|
6246
|
+
'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
|
6247
|
+
'program to start.'
|
6248
|
+
),
|
6249
|
+
'min': self._config.minprocs,
|
6250
|
+
'resource': resource.RLIMIT_NPROC,
|
6251
|
+
'name': 'RLIMIT_NPROC',
|
6252
|
+
})
|
6253
|
+
|
6254
|
+
for limit in limits:
|
6255
|
+
min_limit = limit['min']
|
6256
|
+
res = limit['resource']
|
6257
|
+
msg = limit['msg']
|
6258
|
+
name = limit['name']
|
6259
|
+
|
6260
|
+
soft, hard = resource.getrlimit(res) # type: ignore
|
6261
|
+
|
6262
|
+
# -1 means unlimited
|
6263
|
+
if soft < min_limit and soft != -1: # type: ignore
|
6264
|
+
if hard < min_limit and hard != -1: # type: ignore
|
6265
|
+
# setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
|
6266
|
+
# usage
|
6267
|
+
hard = min_limit # type: ignore
|
6268
|
+
|
6269
|
+
try:
|
6270
|
+
resource.setrlimit(res, (min_limit, hard)) # type: ignore
|
6271
|
+
log.info('Increased %s limit to %s', name, min_limit)
|
6272
|
+
except (resource.error, ValueError):
|
6273
|
+
raise RuntimeError(msg % dict( # type: ignore # noqa
|
6274
|
+
min_limit=min_limit,
|
6275
|
+
res=res,
|
6276
|
+
name=name,
|
6277
|
+
soft=soft,
|
6278
|
+
hard=hard,
|
6279
|
+
))
|
6280
|
+
|
6281
|
+
#
|
6282
|
+
|
6283
|
+
_unlink_pidfile = False
|
6284
|
+
|
6285
|
+
def _write_pidfile(self) -> None:
|
6286
|
+
pid = os.getpid()
|
6287
|
+
try:
|
6288
|
+
with open(self._config.pidfile, 'w') as f:
|
6289
|
+
f.write(f'{pid}\n')
|
6290
|
+
except OSError:
|
6291
|
+
log.critical('could not write pidfile %s', self._config.pidfile)
|
6292
|
+
else:
|
6293
|
+
self._unlink_pidfile = True
|
6294
|
+
log.info('supervisord started with pid %s', pid)
|
6295
|
+
|
6296
|
+
def _cleanup_pidfile(self) -> None:
|
6297
|
+
if self._unlink_pidfile:
|
6298
|
+
try_unlink(self._config.pidfile)
|
6299
|
+
|
6300
|
+
#
|
6301
|
+
|
6302
|
+
def _clear_auto_child_logdir(self) -> None:
|
6303
|
+
# must be called after realize()
|
6304
|
+
child_logdir = self._config.child_logdir
|
6305
|
+
if child_logdir == '/dev/null':
|
6306
|
+
return
|
6307
|
+
|
6308
|
+
fnre = re.compile(rf'.+?---{self._config.identifier}-\S+\.log\.?\d{{0,4}}')
|
6309
|
+
try:
|
6310
|
+
filenames = os.listdir(child_logdir)
|
6311
|
+
except OSError:
|
6312
|
+
log.warning('Could not clear child_log dir')
|
6313
|
+
return
|
6314
|
+
|
6315
|
+
for filename in filenames:
|
6316
|
+
if fnre.match(filename):
|
6317
|
+
pathname = os.path.join(child_logdir, filename)
|
6318
|
+
try:
|
6319
|
+
os.remove(pathname)
|
6320
|
+
except OSError:
|
6321
|
+
log.warning('Failed to clean up %r', pathname)
|
6322
|
+
|
6323
|
+
#
|
6324
|
+
|
6325
|
+
def _daemonize(self) -> None:
|
6326
|
+
for dl in self._daemonize_listeners:
|
6327
|
+
dl.before_daemonize()
|
6328
|
+
|
6329
|
+
self._do_daemonize()
|
6330
|
+
|
6331
|
+
for dl in self._daemonize_listeners:
|
6332
|
+
dl.after_daemonize()
|
6333
|
+
|
6334
|
+
def _do_daemonize(self) -> None:
|
6335
|
+
# To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
|
6336
|
+
# our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
|
6337
|
+
# our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
|
6338
|
+
# terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
|
6339
|
+
# use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
|
6340
|
+
# session and process group and setting itself up as a new session leader.
|
6341
|
+
#
|
6342
|
+
# Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
|
6343
|
+
# of ourselves that is guaranteed to not be a session group leader.
|
6344
|
+
#
|
6345
|
+
# We also change directories, set stderr and stdout to null, and change our umask.
|
6346
|
+
#
|
6347
|
+
# This explanation was (gratefully) garnered from
|
6348
|
+
# http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
|
6349
|
+
|
6350
|
+
pid = os.fork()
|
6351
|
+
if pid != 0:
|
6352
|
+
# Parent
|
6353
|
+
log.debug('supervisord forked; parent exiting')
|
6354
|
+
real_exit(0)
|
6355
|
+
|
6356
|
+
# Child
|
6357
|
+
log.info('daemonizing the supervisord process')
|
6358
|
+
if self._config.directory:
|
6359
|
+
try:
|
6360
|
+
os.chdir(self._config.directory)
|
6361
|
+
except OSError as err:
|
6362
|
+
log.critical("can't chdir into %r: %s", self._config.directory, err)
|
6363
|
+
else:
|
6364
|
+
log.info('set current directory: %r', self._config.directory)
|
6365
|
+
|
6366
|
+
os.dup2(0, os.open('/dev/null', os.O_RDONLY))
|
6367
|
+
os.dup2(1, os.open('/dev/null', os.O_WRONLY))
|
6368
|
+
os.dup2(2, os.open('/dev/null', os.O_WRONLY))
|
6369
|
+
|
6370
|
+
# XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
|
6371
|
+
# file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
|
6372
|
+
# again after the setsid() call, for obscure SVR4 reasons.
|
6373
|
+
os.setsid()
|
6374
|
+
os.umask(self._config.umask)
|
6375
|
+
|
6376
|
+
|
6377
|
+
########################################
|
6378
|
+
# ../spawning.py
|
6379
|
+
|
6380
|
+
|
6381
|
+
@dc.dataclass(frozen=True)
|
6382
|
+
class SpawnedProcess:
|
6383
|
+
pid: int
|
6384
|
+
pipes: ProcessPipes
|
6385
|
+
dispatchers: Dispatchers
|
6386
|
+
|
6387
|
+
|
6388
|
+
class ProcessSpawnError(RuntimeError):
|
6389
|
+
pass
|
6390
|
+
|
6391
|
+
|
6392
|
+
class ProcessSpawning:
|
6393
|
+
@property
|
6394
|
+
@abc.abstractmethod
|
6395
|
+
def process(self) -> Process:
|
6396
|
+
raise NotImplementedError
|
6397
|
+
|
6398
|
+
#
|
6399
|
+
|
6400
|
+
@abc.abstractmethod
|
6401
|
+
def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
|
6402
|
+
raise NotImplementedError
|
6403
|
+
|
6404
|
+
|
6405
|
+
########################################
|
6406
|
+
# ../supervisor.py
|
6407
|
+
|
6408
|
+
|
6409
|
+
##
|
6410
|
+
|
6411
|
+
|
6412
|
+
class SignalHandler:
|
6413
|
+
def __init__(
|
6414
|
+
self,
|
6415
|
+
*,
|
6416
|
+
context: ServerContextImpl,
|
6417
|
+
signal_receiver: SignalReceiver,
|
6418
|
+
process_groups: ProcessGroupManager,
|
6419
|
+
) -> None:
|
6420
|
+
super().__init__()
|
6421
|
+
|
6422
|
+
self._context = context
|
6423
|
+
self._signal_receiver = signal_receiver
|
6424
|
+
self._process_groups = process_groups
|
6425
|
+
|
6426
|
+
def set_signals(self) -> None:
|
6427
|
+
self._signal_receiver.install(
|
6428
|
+
signal.SIGTERM,
|
6429
|
+
signal.SIGINT,
|
6430
|
+
signal.SIGQUIT,
|
6431
|
+
signal.SIGHUP,
|
6432
|
+
signal.SIGCHLD,
|
6433
|
+
signal.SIGUSR2,
|
6434
|
+
)
|
6435
|
+
|
6436
|
+
def handle_signals(self) -> None:
|
6437
|
+
sig = self._signal_receiver.get_signal()
|
6438
|
+
if not sig:
|
6439
|
+
return
|
6440
|
+
|
6441
|
+
if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
|
6442
|
+
log.warning('received %s indicating exit request', sig_name(sig))
|
6443
|
+
self._context.set_state(SupervisorState.SHUTDOWN)
|
6444
|
+
|
6445
|
+
elif sig == signal.SIGHUP:
|
6446
|
+
if self._context.state == SupervisorState.SHUTDOWN:
|
6447
|
+
log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
|
6448
|
+
else:
|
6449
|
+
log.warning('received %s indicating restart request', sig_name(sig)) # noqa
|
6450
|
+
self._context.set_state(SupervisorState.RESTARTING)
|
6451
|
+
|
6452
|
+
elif sig == signal.SIGCHLD:
|
6453
|
+
log.debug('received %s indicating a child quit', sig_name(sig))
|
6454
|
+
|
6455
|
+
elif sig == signal.SIGUSR2:
|
6456
|
+
log.info('received %s indicating log reopen request', sig_name(sig))
|
6457
|
+
|
6458
|
+
for p in self._process_groups.all_processes():
|
6459
|
+
for d in p.get_dispatchers():
|
6460
|
+
if isinstance(d, OutputDispatcher):
|
6461
|
+
d.reopen_logs()
|
6462
|
+
|
6463
|
+
else:
|
6464
|
+
log.debug('received %s indicating nothing', sig_name(sig))
|
6465
|
+
|
6466
|
+
|
6467
|
+
##
|
6468
|
+
|
6469
|
+
|
6470
|
+
class ProcessGroupFactory(Func1[ProcessGroupConfig, ProcessGroup]):
|
6471
|
+
pass
|
6472
|
+
|
6473
|
+
|
6474
|
+
class Supervisor:
|
6475
|
+
def __init__(
|
6476
|
+
self,
|
6477
|
+
*,
|
6478
|
+
context: ServerContextImpl,
|
6479
|
+
poller: Poller,
|
6480
|
+
process_groups: ProcessGroupManager,
|
6481
|
+
signal_handler: SignalHandler,
|
6482
|
+
event_callbacks: EventCallbacks,
|
6483
|
+
process_group_factory: ProcessGroupFactory,
|
6484
|
+
pid_history: PidHistory,
|
6485
|
+
setup: SupervisorSetup,
|
6486
|
+
) -> None:
|
6487
|
+
super().__init__()
|
6488
|
+
|
6489
|
+
self._context = context
|
6490
|
+
self._poller = poller
|
6491
|
+
self._process_groups = process_groups
|
6492
|
+
self._signal_handler = signal_handler
|
6493
|
+
self._event_callbacks = event_callbacks
|
6494
|
+
self._process_group_factory = process_group_factory
|
6495
|
+
self._pid_history = pid_history
|
6496
|
+
self._setup = setup
|
6497
|
+
|
6498
|
+
self._ticks: ta.Dict[int, float] = {}
|
6499
|
+
self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
|
6500
|
+
self._stopping = False # set after we detect that we are handling a stop request
|
6501
|
+
self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
|
6502
|
+
|
6503
|
+
#
|
6504
|
+
|
6505
|
+
@property
|
6506
|
+
def context(self) -> ServerContextImpl:
|
6507
|
+
return self._context
|
6508
|
+
|
6509
|
+
def get_state(self) -> SupervisorState:
|
6510
|
+
return self._context.state
|
6511
|
+
|
6512
|
+
#
|
6513
|
+
|
6514
|
+
def add_process_group(self, config: ProcessGroupConfig) -> bool:
|
6515
|
+
if self._process_groups.get(config.name) is not None:
|
6516
|
+
return False
|
6517
|
+
|
6518
|
+
group = check_isinstance(self._process_group_factory(config), ProcessGroup)
|
6519
|
+
for process in group:
|
6520
|
+
process.after_setuid()
|
6521
|
+
|
6522
|
+
self._process_groups.add(group)
|
6523
|
+
|
6524
|
+
return True
|
5892
6525
|
|
5893
|
-
|
6526
|
+
def remove_process_group(self, name: str) -> bool:
|
6527
|
+
if self._process_groups[name].get_unstopped_processes():
|
6528
|
+
return False
|
5894
6529
|
|
5895
|
-
|
6530
|
+
self._process_groups.remove(name)
|
5896
6531
|
|
5897
|
-
|
6532
|
+
return True
|
5898
6533
|
|
5899
|
-
|
5900
|
-
# FIXME: events?
|
5901
|
-
self._by_name.clear()
|
6534
|
+
#
|
5902
6535
|
|
6536
|
+
def shutdown_report(self) -> ta.List[Process]:
|
6537
|
+
unstopped: ta.List[Process] = []
|
5903
6538
|
|
5904
|
-
|
5905
|
-
|
6539
|
+
for group in self._process_groups:
|
6540
|
+
unstopped.extend(group.get_unstopped_processes())
|
5906
6541
|
|
6542
|
+
if unstopped:
|
6543
|
+
# throttle 'waiting for x to die' reports
|
6544
|
+
now = time.time()
|
6545
|
+
if now > (self._last_shutdown_report + 3): # every 3 secs
|
6546
|
+
names = [as_string(p.config.name) for p in unstopped]
|
6547
|
+
namestr = ', '.join(names)
|
6548
|
+
log.info('waiting for %s to die', namestr)
|
6549
|
+
self._last_shutdown_report = now
|
6550
|
+
for proc in unstopped:
|
6551
|
+
log.debug('%s state: %s', proc.config.name, proc.get_state().name)
|
5907
6552
|
|
5908
|
-
|
5909
|
-
OutputDispatcherFactory = ta.NewType('OutputDispatcherFactory', Func[OutputDispatcher])
|
6553
|
+
return unstopped
|
5910
6554
|
|
5911
|
-
#
|
5912
|
-
InputDispatcherFactory = ta.NewType('InputDispatcherFactory', Func[InputDispatcher])
|
6555
|
+
#
|
5913
6556
|
|
5914
|
-
|
6557
|
+
def main(self, **kwargs: ta.Any) -> None:
|
6558
|
+
self._setup.setup()
|
6559
|
+
try:
|
6560
|
+
self.run(**kwargs)
|
6561
|
+
finally:
|
6562
|
+
self._setup.cleanup()
|
6563
|
+
|
6564
|
+
def run(
|
6565
|
+
self,
|
6566
|
+
*,
|
6567
|
+
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
6568
|
+
) -> None:
|
6569
|
+
self._process_groups.clear()
|
6570
|
+
self._stop_groups = None # clear
|
6571
|
+
|
6572
|
+
self._event_callbacks.clear()
|
6573
|
+
|
6574
|
+
try:
|
6575
|
+
for config in self._context.config.groups or []:
|
6576
|
+
self.add_process_group(config)
|
6577
|
+
|
6578
|
+
self._signal_handler.set_signals()
|
6579
|
+
|
6580
|
+
self._event_callbacks.notify(SupervisorRunningEvent())
|
6581
|
+
|
6582
|
+
while True:
|
6583
|
+
if callback is not None and not callback(self):
|
6584
|
+
break
|
6585
|
+
|
6586
|
+
self._run_once()
|
6587
|
+
|
6588
|
+
finally:
|
6589
|
+
self._poller.close()
|
6590
|
+
|
6591
|
+
#
|
6592
|
+
|
6593
|
+
def _run_once(self) -> None:
|
6594
|
+
self._poll()
|
6595
|
+
self._reap()
|
6596
|
+
self._signal_handler.handle_signals()
|
6597
|
+
self._tick()
|
6598
|
+
|
6599
|
+
if self._context.state < SupervisorState.RUNNING:
|
6600
|
+
self._ordered_stop_groups_phase_2()
|
6601
|
+
|
6602
|
+
def _ordered_stop_groups_phase_1(self) -> None:
|
6603
|
+
if self._stop_groups:
|
6604
|
+
# stop the last group (the one with the "highest" priority)
|
6605
|
+
self._stop_groups[-1].stop_all()
|
6606
|
+
|
6607
|
+
def _ordered_stop_groups_phase_2(self) -> None:
|
6608
|
+
# after phase 1 we've transitioned and reaped, let's see if we can remove the group we stopped from the
|
6609
|
+
# stop_groups queue.
|
6610
|
+
if self._stop_groups:
|
6611
|
+
# pop the last group (the one with the "highest" priority)
|
6612
|
+
group = self._stop_groups.pop()
|
6613
|
+
if group.get_unstopped_processes():
|
6614
|
+
# if any processes in the group aren't yet in a stopped state, we're not yet done shutting this group
|
6615
|
+
# down, so push it back on to the end of the stop group queue
|
6616
|
+
self._stop_groups.append(group)
|
6617
|
+
|
6618
|
+
def get_dispatchers(self) -> Dispatchers:
|
6619
|
+
return Dispatchers(
|
6620
|
+
d
|
6621
|
+
for p in self._process_groups.all_processes()
|
6622
|
+
for d in p.get_dispatchers()
|
6623
|
+
)
|
6624
|
+
|
6625
|
+
def _poll(self) -> None:
|
6626
|
+
dispatchers = self.get_dispatchers()
|
6627
|
+
|
6628
|
+
sorted_groups = list(self._process_groups)
|
6629
|
+
sorted_groups.sort()
|
6630
|
+
|
6631
|
+
if self._context.state < SupervisorState.RUNNING:
|
6632
|
+
if not self._stopping:
|
6633
|
+
# first time, set the stopping flag, do a notification and set stop_groups
|
6634
|
+
self._stopping = True
|
6635
|
+
self._stop_groups = sorted_groups[:]
|
6636
|
+
self._event_callbacks.notify(SupervisorStoppingEvent())
|
6637
|
+
|
6638
|
+
self._ordered_stop_groups_phase_1()
|
6639
|
+
|
6640
|
+
if not self.shutdown_report():
|
6641
|
+
# if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
|
6642
|
+
raise ExitNow
|
6643
|
+
|
6644
|
+
for fd, dispatcher in dispatchers.items():
|
6645
|
+
if dispatcher.readable():
|
6646
|
+
self._poller.register_readable(fd)
|
6647
|
+
if dispatcher.writable():
|
6648
|
+
self._poller.register_writable(fd)
|
6649
|
+
|
6650
|
+
timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
|
6651
|
+
r, w = self._poller.poll(timeout)
|
6652
|
+
|
6653
|
+
for fd in r:
|
6654
|
+
if fd in dispatchers:
|
6655
|
+
try:
|
6656
|
+
dispatcher = dispatchers[fd]
|
6657
|
+
log.debug('read event caused by %r', dispatcher)
|
6658
|
+
dispatcher.handle_read_event()
|
6659
|
+
if not dispatcher.readable():
|
6660
|
+
self._poller.unregister_readable(fd)
|
6661
|
+
except ExitNow:
|
6662
|
+
raise
|
6663
|
+
except Exception: # noqa
|
6664
|
+
dispatchers[fd].handle_error()
|
6665
|
+
else:
|
6666
|
+
# if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
|
6667
|
+
# time, which may cause 100% cpu usage
|
6668
|
+
log.debug('unexpected read event from fd %r', fd)
|
6669
|
+
try:
|
6670
|
+
self._poller.unregister_readable(fd)
|
6671
|
+
except Exception: # noqa
|
6672
|
+
pass
|
6673
|
+
|
6674
|
+
for fd in w:
|
6675
|
+
if fd in dispatchers:
|
6676
|
+
try:
|
6677
|
+
dispatcher = dispatchers[fd]
|
6678
|
+
log.debug('write event caused by %r', dispatcher)
|
6679
|
+
dispatcher.handle_write_event()
|
6680
|
+
if not dispatcher.writable():
|
6681
|
+
self._poller.unregister_writable(fd)
|
6682
|
+
except ExitNow:
|
6683
|
+
raise
|
6684
|
+
except Exception: # noqa
|
6685
|
+
dispatchers[fd].handle_error()
|
6686
|
+
else:
|
6687
|
+
log.debug('unexpected write event from fd %r', fd)
|
6688
|
+
try:
|
6689
|
+
self._poller.unregister_writable(fd)
|
6690
|
+
except Exception: # noqa
|
6691
|
+
pass
|
6692
|
+
|
6693
|
+
for group in sorted_groups:
|
6694
|
+
for process in group:
|
6695
|
+
process.transition()
|
6696
|
+
|
6697
|
+
def _reap(self, *, once: bool = False, depth: int = 0) -> None:
|
6698
|
+
if depth >= 100:
|
6699
|
+
return
|
6700
|
+
|
6701
|
+
pid, sts = self._context.waitpid()
|
6702
|
+
if not pid:
|
6703
|
+
return
|
6704
|
+
|
6705
|
+
process = self._pid_history.get(pid, None)
|
6706
|
+
if process is None:
|
6707
|
+
_, msg = decode_wait_status(check_not_none(sts))
|
6708
|
+
log.info('reaped unknown pid %s (%s)', pid, msg)
|
6709
|
+
else:
|
6710
|
+
process.finish(check_not_none(sts))
|
6711
|
+
del self._pid_history[pid]
|
6712
|
+
|
6713
|
+
if not once:
|
6714
|
+
# keep reaping until no more kids to reap, but don't recurse infinitely
|
6715
|
+
self._reap(once=False, depth=depth + 1)
|
6716
|
+
|
6717
|
+
def _tick(self, now: ta.Optional[float] = None) -> None:
|
6718
|
+
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
6719
|
+
|
6720
|
+
if now is None:
|
6721
|
+
# now won't be None in unit tests
|
6722
|
+
now = time.time()
|
6723
|
+
|
6724
|
+
for event in TICK_EVENTS:
|
6725
|
+
period = event.period
|
6726
|
+
|
6727
|
+
last_tick = self._ticks.get(period)
|
6728
|
+
if last_tick is None:
|
6729
|
+
# we just started up
|
6730
|
+
last_tick = self._ticks[period] = timeslice(period, now)
|
6731
|
+
|
6732
|
+
this_tick = timeslice(period, now)
|
6733
|
+
if this_tick != last_tick:
|
6734
|
+
self._ticks[period] = this_tick
|
6735
|
+
self._event_callbacks.notify(event(this_tick, self))
|
6736
|
+
|
6737
|
+
|
6738
|
+
########################################
|
6739
|
+
# ../processesimpl.py
|
6740
|
+
|
6741
|
+
|
6742
|
+
class ProcessSpawningFactory(Func1[Process, ProcessSpawning]):
|
6743
|
+
pass
|
5915
6744
|
|
5916
6745
|
|
5917
6746
|
##
|
@@ -5927,12 +6756,7 @@ class ProcessImpl(Process):
|
|
5927
6756
|
*,
|
5928
6757
|
context: ServerContext,
|
5929
6758
|
event_callbacks: EventCallbacks,
|
5930
|
-
|
5931
|
-
output_dispatcher_factory: OutputDispatcherFactory,
|
5932
|
-
input_dispatcher_factory: InputDispatcherFactory,
|
5933
|
-
|
5934
|
-
inherited_fds: ta.Optional[InheritedFds] = None,
|
5935
|
-
|
6759
|
+
process_spawning_factory: ProcessSpawningFactory,
|
5936
6760
|
) -> None:
|
5937
6761
|
super().__init__()
|
5938
6762
|
|
@@ -5942,13 +6766,12 @@ class ProcessImpl(Process):
|
|
5942
6766
|
self._context = context
|
5943
6767
|
self._event_callbacks = event_callbacks
|
5944
6768
|
|
5945
|
-
self.
|
5946
|
-
self._input_dispatcher_factory = input_dispatcher_factory
|
6769
|
+
self._spawning = process_spawning_factory(self)
|
5947
6770
|
|
5948
|
-
|
6771
|
+
#
|
5949
6772
|
|
5950
|
-
self._dispatchers
|
5951
|
-
self._pipes
|
6773
|
+
self._dispatchers = Dispatchers([])
|
6774
|
+
self._pipes = ProcessPipes()
|
5952
6775
|
|
5953
6776
|
self._state = ProcessState.STOPPED
|
5954
6777
|
self._pid = 0 # 0 when not running
|
@@ -5968,141 +6791,44 @@ class ProcessImpl(Process):
|
|
5968
6791
|
self._exitstatus: ta.Optional[int] = None # status attached to dead process by finish()
|
5969
6792
|
self._spawn_err: ta.Optional[str] = None # error message attached by spawn() if any
|
5970
6793
|
|
5971
|
-
|
5972
|
-
def pid(self) -> int:
|
5973
|
-
return self._pid
|
5974
|
-
|
5975
|
-
@property
|
5976
|
-
def group(self) -> ProcessGroup:
|
5977
|
-
return self._group
|
5978
|
-
|
5979
|
-
@property
|
5980
|
-
def config(self) -> ProcessConfig:
|
5981
|
-
return self._config
|
5982
|
-
|
5983
|
-
@property
|
5984
|
-
def context(self) -> ServerContext:
|
5985
|
-
return self._context
|
5986
|
-
|
5987
|
-
@property
|
5988
|
-
def state(self) -> ProcessState:
|
5989
|
-
return self._state
|
5990
|
-
|
5991
|
-
@property
|
5992
|
-
def backoff(self) -> int:
|
5993
|
-
return self._backoff
|
5994
|
-
|
5995
|
-
def get_dispatchers(self) -> ta.Mapping[int, Dispatcher]:
|
5996
|
-
return self._dispatchers
|
5997
|
-
|
5998
|
-
def remove_logs(self) -> None:
|
5999
|
-
for dispatcher in self._dispatchers.values():
|
6000
|
-
if hasattr(dispatcher, 'remove_logs'):
|
6001
|
-
dispatcher.remove_logs()
|
6002
|
-
|
6003
|
-
def reopen_logs(self) -> None:
|
6004
|
-
for dispatcher in self._dispatchers.values():
|
6005
|
-
if hasattr(dispatcher, 'reopen_logs'):
|
6006
|
-
dispatcher.reopen_logs()
|
6007
|
-
|
6008
|
-
def drain(self) -> None:
|
6009
|
-
for dispatcher in self._dispatchers.values():
|
6010
|
-
# note that we *must* call readable() for every dispatcher, as it may have side effects for a given
|
6011
|
-
# dispatcher (eg. call handle_listener_state_change for event listener processes)
|
6012
|
-
if dispatcher.readable():
|
6013
|
-
dispatcher.handle_read_event()
|
6014
|
-
if dispatcher.writable():
|
6015
|
-
dispatcher.handle_write_event()
|
6016
|
-
|
6017
|
-
def write(self, chars: ta.Union[bytes, str]) -> None:
|
6018
|
-
if not self.pid or self._killing:
|
6019
|
-
raise OSError(errno.EPIPE, 'Process already closed')
|
6020
|
-
|
6021
|
-
stdin_fd = self._pipes['stdin']
|
6022
|
-
if stdin_fd is None:
|
6023
|
-
raise OSError(errno.EPIPE, 'Process has no stdin channel')
|
6024
|
-
|
6025
|
-
dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
|
6026
|
-
if dispatcher.closed:
|
6027
|
-
raise OSError(errno.EPIPE, "Process' stdin channel is closed")
|
6028
|
-
|
6029
|
-
dispatcher.write(chars)
|
6030
|
-
dispatcher.flush() # this must raise EPIPE if the pipe is closed
|
6031
|
-
|
6032
|
-
def _get_execv_args(self) -> ta.Tuple[str, ta.Sequence[str]]:
|
6033
|
-
"""
|
6034
|
-
Internal: turn a program name into a file name, using $PATH, make sure it exists / is executable, raising a
|
6035
|
-
ProcessError if not
|
6036
|
-
"""
|
6037
|
-
|
6038
|
-
try:
|
6039
|
-
commandargs = shlex.split(self._config.command)
|
6040
|
-
except ValueError as e:
|
6041
|
-
raise BadCommandError(f"can't parse command {self._config.command!r}: {e}") # noqa
|
6794
|
+
#
|
6042
6795
|
|
6043
|
-
|
6044
|
-
|
6045
|
-
else:
|
6046
|
-
raise BadCommandError('command is empty')
|
6796
|
+
def __repr__(self) -> str:
|
6797
|
+
return f'<Subprocess at {id(self)} with name {self._config.name} in state {self.get_state().name}>'
|
6047
6798
|
|
6048
|
-
|
6049
|
-
filename = program
|
6050
|
-
try:
|
6051
|
-
st = os.stat(filename)
|
6052
|
-
except OSError:
|
6053
|
-
st = None
|
6799
|
+
#
|
6054
6800
|
|
6055
|
-
|
6056
|
-
|
6057
|
-
|
6058
|
-
st = None
|
6059
|
-
for dir in path: # noqa
|
6060
|
-
found = os.path.join(dir, program)
|
6061
|
-
try:
|
6062
|
-
st = os.stat(found)
|
6063
|
-
except OSError:
|
6064
|
-
pass
|
6065
|
-
else:
|
6066
|
-
break
|
6067
|
-
if st is None:
|
6068
|
-
filename = program
|
6069
|
-
else:
|
6070
|
-
filename = found # type: ignore
|
6801
|
+
@property
|
6802
|
+
def name(self) -> str:
|
6803
|
+
return self._config.name
|
6071
6804
|
|
6072
|
-
|
6073
|
-
|
6074
|
-
|
6805
|
+
@property
|
6806
|
+
def config(self) -> ProcessConfig:
|
6807
|
+
return self._config
|
6075
6808
|
|
6076
|
-
|
6809
|
+
@property
|
6810
|
+
def group(self) -> ProcessGroup:
|
6811
|
+
return self._group
|
6077
6812
|
|
6078
|
-
|
6079
|
-
|
6080
|
-
|
6081
|
-
return False
|
6813
|
+
@property
|
6814
|
+
def pid(self) -> int:
|
6815
|
+
return self._pid
|
6082
6816
|
|
6083
|
-
|
6084
|
-
if new_state == ProcessState.BACKOFF:
|
6085
|
-
now = time.time()
|
6086
|
-
self._backoff += 1
|
6087
|
-
self._delay = now + self._backoff
|
6817
|
+
#
|
6088
6818
|
|
6089
|
-
|
6090
|
-
|
6091
|
-
|
6092
|
-
self._event_callbacks.notify(event)
|
6819
|
+
@property
|
6820
|
+
def context(self) -> ServerContext:
|
6821
|
+
return self._context
|
6093
6822
|
|
6094
|
-
|
6823
|
+
@property
|
6824
|
+
def state(self) -> ProcessState:
|
6825
|
+
return self._state
|
6095
6826
|
|
6096
|
-
|
6097
|
-
|
6098
|
-
|
6099
|
-
allowable_states = ' '.join(s.name for s in states)
|
6100
|
-
process_name = as_string(self._config.name)
|
6101
|
-
raise RuntimeError('Assertion failed for %s: %s not in %s' % (process_name, current_state, allowable_states)) # noqa
|
6827
|
+
@property
|
6828
|
+
def backoff(self) -> int:
|
6829
|
+
return self._backoff
|
6102
6830
|
|
6103
|
-
|
6104
|
-
self._spawn_err = msg
|
6105
|
-
log.info('_spawn_err: %s', msg)
|
6831
|
+
#
|
6106
6832
|
|
6107
6833
|
def spawn(self) -> ta.Optional[int]:
|
6108
6834
|
process_name = as_string(self._config.name)
|
@@ -6111,6 +6837,13 @@ class ProcessImpl(Process):
|
|
6111
6837
|
log.warning('process \'%s\' already running', process_name)
|
6112
6838
|
return None
|
6113
6839
|
|
6840
|
+
self.check_in_state(
|
6841
|
+
ProcessState.EXITED,
|
6842
|
+
ProcessState.FATAL,
|
6843
|
+
ProcessState.BACKOFF,
|
6844
|
+
ProcessState.STOPPED,
|
6845
|
+
)
|
6846
|
+
|
6114
6847
|
self._killing = False
|
6115
6848
|
self._spawn_err = None
|
6116
6849
|
self._exitstatus = None
|
@@ -6119,183 +6852,73 @@ class ProcessImpl(Process):
|
|
6119
6852
|
|
6120
6853
|
self._last_start = time.time()
|
6121
6854
|
|
6122
|
-
self._check_in_state(
|
6123
|
-
ProcessState.EXITED,
|
6124
|
-
ProcessState.FATAL,
|
6125
|
-
ProcessState.BACKOFF,
|
6126
|
-
ProcessState.STOPPED,
|
6127
|
-
)
|
6128
|
-
|
6129
6855
|
self.change_state(ProcessState.STARTING)
|
6130
6856
|
|
6131
6857
|
try:
|
6132
|
-
|
6133
|
-
except
|
6134
|
-
|
6135
|
-
self.
|
6136
|
-
self.
|
6137
|
-
return None
|
6138
|
-
|
6139
|
-
try:
|
6140
|
-
self._dispatchers, self._pipes = self._make_dispatchers() # type: ignore
|
6141
|
-
except OSError as why:
|
6142
|
-
code = why.args[0]
|
6143
|
-
if code == errno.EMFILE:
|
6144
|
-
# too many file descriptors open
|
6145
|
-
msg = f"too many open files to spawn '{process_name}'"
|
6146
|
-
else:
|
6147
|
-
msg = f"unknown error making dispatchers for '{process_name}': {errno.errorcode.get(code, code)}"
|
6148
|
-
self._record_spawn_err(msg)
|
6149
|
-
self._check_in_state(ProcessState.STARTING)
|
6150
|
-
self.change_state(ProcessState.BACKOFF)
|
6151
|
-
return None
|
6152
|
-
|
6153
|
-
try:
|
6154
|
-
pid = os.fork()
|
6155
|
-
except OSError as why:
|
6156
|
-
code = why.args[0]
|
6157
|
-
if code == errno.EAGAIN:
|
6158
|
-
# process table full
|
6159
|
-
msg = f'Too many processes in process table to spawn \'{process_name}\''
|
6160
|
-
else:
|
6161
|
-
msg = f'unknown error during fork for \'{process_name}\': {errno.errorcode.get(code, code)}'
|
6162
|
-
self._record_spawn_err(msg)
|
6163
|
-
self._check_in_state(ProcessState.STARTING)
|
6858
|
+
sp = self._spawning.spawn()
|
6859
|
+
except ProcessSpawnError as err:
|
6860
|
+
log.exception('Spawn error')
|
6861
|
+
self._spawn_err = err.args[0]
|
6862
|
+
self.check_in_state(ProcessState.STARTING)
|
6164
6863
|
self.change_state(ProcessState.BACKOFF)
|
6165
|
-
close_parent_pipes(self._pipes)
|
6166
|
-
close_child_pipes(self._pipes)
|
6167
6864
|
return None
|
6168
6865
|
|
6169
|
-
|
6170
|
-
return self._spawn_as_parent(pid)
|
6171
|
-
|
6172
|
-
else:
|
6173
|
-
self._spawn_as_child(filename, argv)
|
6174
|
-
return None
|
6866
|
+
log.info("Spawned: '%s' with pid %s", self.name, sp.pid)
|
6175
6867
|
|
6176
|
-
|
6177
|
-
|
6868
|
+
self._pid = sp.pid
|
6869
|
+
self._pipes = sp.pipes
|
6870
|
+
self._dispatchers = sp.dispatchers
|
6178
6871
|
|
6179
|
-
|
6180
|
-
stdout_fd, stderr_fd, stdin_fd = p['stdout'], p['stderr'], p['stdin']
|
6872
|
+
self._delay = time.time() + self.config.startsecs
|
6181
6873
|
|
6182
|
-
|
6874
|
+
return sp.pid
|
6183
6875
|
|
6184
|
-
|
6185
|
-
|
6186
|
-
)
|
6876
|
+
def get_dispatchers(self) -> Dispatchers:
|
6877
|
+
return self._dispatchers
|
6187
6878
|
|
6188
|
-
|
6189
|
-
if
|
6190
|
-
|
6191
|
-
dispatchers[stdout_fd] = check_isinstance(self._output_dispatcher_factory(
|
6192
|
-
self,
|
6193
|
-
etype,
|
6194
|
-
stdout_fd,
|
6195
|
-
**dispatcher_kw,
|
6196
|
-
), OutputDispatcher)
|
6197
|
-
|
6198
|
-
if stderr_fd is not None:
|
6199
|
-
etype = ProcessCommunicationStderrEvent
|
6200
|
-
dispatchers[stderr_fd] = check_isinstance(self._output_dispatcher_factory(
|
6201
|
-
self,
|
6202
|
-
etype,
|
6203
|
-
stderr_fd,
|
6204
|
-
**dispatcher_kw,
|
6205
|
-
), OutputDispatcher)
|
6206
|
-
|
6207
|
-
if stdin_fd is not None:
|
6208
|
-
dispatchers[stdin_fd] = check_isinstance(self._input_dispatcher_factory(
|
6209
|
-
self,
|
6210
|
-
'stdin',
|
6211
|
-
stdin_fd,
|
6212
|
-
**dispatcher_kw,
|
6213
|
-
), InputDispatcher)
|
6879
|
+
def write(self, chars: ta.Union[bytes, str]) -> None:
|
6880
|
+
if not self.pid or self._killing:
|
6881
|
+
raise OSError(errno.EPIPE, 'Process already closed')
|
6214
6882
|
|
6215
|
-
|
6883
|
+
stdin_fd = self._pipes.stdin
|
6884
|
+
if stdin_fd is None:
|
6885
|
+
raise OSError(errno.EPIPE, 'Process has no stdin channel')
|
6216
6886
|
|
6217
|
-
|
6218
|
-
|
6219
|
-
|
6220
|
-
close_child_pipes(self._pipes)
|
6221
|
-
log.info('spawned: \'%s\' with pid %s', as_string(self._config.name), pid)
|
6222
|
-
self._spawn_err = None
|
6223
|
-
self._delay = time.time() + self._config.startsecs
|
6224
|
-
self.context.pid_history[pid] = self
|
6225
|
-
return pid
|
6226
|
-
|
6227
|
-
def _prepare_child_fds(self) -> None:
|
6228
|
-
os.dup2(self._pipes['child_stdin'], 0)
|
6229
|
-
os.dup2(self._pipes['child_stdout'], 1)
|
6230
|
-
if self._config.redirect_stderr:
|
6231
|
-
os.dup2(self._pipes['child_stdout'], 2)
|
6232
|
-
else:
|
6233
|
-
os.dup2(self._pipes['child_stderr'], 2)
|
6887
|
+
dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
|
6888
|
+
if dispatcher.closed:
|
6889
|
+
raise OSError(errno.EPIPE, "Process' stdin channel is closed")
|
6234
6890
|
|
6235
|
-
|
6236
|
-
|
6237
|
-
continue
|
6238
|
-
close_fd(i)
|
6891
|
+
dispatcher.write(chars)
|
6892
|
+
dispatcher.flush() # this must raise EPIPE if the pipe is closed
|
6239
6893
|
|
6240
|
-
|
6241
|
-
try:
|
6242
|
-
# prevent child from receiving signals sent to the parent by calling os.setpgrp to create a new process
|
6243
|
-
# group for the child; this prevents, for instance, the case of child processes being sent a SIGINT when
|
6244
|
-
# running supervisor in foreground mode and Ctrl-C in the terminal window running supervisord is pressed.
|
6245
|
-
# Presumably it also prevents HUP, etc received by supervisord from being sent to children.
|
6246
|
-
os.setpgrp()
|
6894
|
+
#
|
6247
6895
|
|
6248
|
-
|
6249
|
-
|
6896
|
+
def change_state(self, new_state: ProcessState, expected: bool = True) -> bool:
|
6897
|
+
old_state = self._state
|
6898
|
+
if new_state is old_state:
|
6899
|
+
return False
|
6250
6900
|
|
6251
|
-
|
6252
|
-
|
6253
|
-
|
6254
|
-
|
6255
|
-
|
6256
|
-
os.write(2, as_bytes('supervisor: ' + msg))
|
6257
|
-
return # finally clause will exit the child process
|
6901
|
+
self._state = new_state
|
6902
|
+
if new_state == ProcessState.BACKOFF:
|
6903
|
+
now = time.time()
|
6904
|
+
self._backoff += 1
|
6905
|
+
self._delay = now + self._backoff
|
6258
6906
|
|
6259
|
-
|
6260
|
-
|
6261
|
-
|
6262
|
-
|
6263
|
-
if self._group:
|
6264
|
-
env['SUPERVISOR_GROUP_NAME'] = self._group.config.name
|
6265
|
-
if self._config.environment is not None:
|
6266
|
-
env.update(self._config.environment)
|
6267
|
-
|
6268
|
-
# change directory
|
6269
|
-
cwd = self._config.directory
|
6270
|
-
try:
|
6271
|
-
if cwd is not None:
|
6272
|
-
os.chdir(os.path.expanduser(cwd))
|
6273
|
-
except OSError as why:
|
6274
|
-
code = errno.errorcode.get(why.args[0], why.args[0])
|
6275
|
-
msg = f"couldn't chdir to {cwd}: {code}\n"
|
6276
|
-
os.write(2, as_bytes('supervisor: ' + msg))
|
6277
|
-
return # finally clause will exit the child process
|
6907
|
+
event_class = PROCESS_STATE_EVENT_MAP.get(new_state)
|
6908
|
+
if event_class is not None:
|
6909
|
+
event = event_class(self, old_state, expected)
|
6910
|
+
self._event_callbacks.notify(event)
|
6278
6911
|
|
6279
|
-
|
6280
|
-
try:
|
6281
|
-
if self._config.umask is not None:
|
6282
|
-
os.umask(self._config.umask)
|
6283
|
-
os.execve(filename, list(argv), env)
|
6284
|
-
except OSError as why:
|
6285
|
-
code = errno.errorcode.get(why.args[0], why.args[0])
|
6286
|
-
msg = f"couldn't exec {argv[0]}: {code}\n"
|
6287
|
-
os.write(2, as_bytes('supervisor: ' + msg))
|
6288
|
-
except Exception: # noqa
|
6289
|
-
(file, fun, line), t, v, tbinfo = compact_traceback()
|
6290
|
-
error = f'{t}, {v}: file: {file} line: {line}'
|
6291
|
-
msg = f"couldn't exec {filename}: {error}\n"
|
6292
|
-
os.write(2, as_bytes('supervisor: ' + msg))
|
6912
|
+
return True
|
6293
6913
|
|
6294
|
-
|
6914
|
+
def check_in_state(self, *states: ProcessState) -> None:
|
6915
|
+
if self._state not in states:
|
6916
|
+
raise ProcessStateError(
|
6917
|
+
f'Check failed for {self._config.name}: '
|
6918
|
+
f'{self._state.name} not in {" ".join(s.name for s in states)}',
|
6919
|
+
)
|
6295
6920
|
|
6296
|
-
|
6297
|
-
os.write(2, as_bytes('supervisor: child process was not spawned\n'))
|
6298
|
-
real_exit(127) # exit process with code for spawn failure
|
6921
|
+
#
|
6299
6922
|
|
6300
6923
|
def _check_and_adjust_for_system_clock_rollback(self, test_time):
|
6301
6924
|
"""
|
@@ -6341,7 +6964,7 @@ class ProcessImpl(Process):
|
|
6341
6964
|
self._delay = 0
|
6342
6965
|
self._backoff = 0
|
6343
6966
|
self._system_stop = True
|
6344
|
-
self.
|
6967
|
+
self.check_in_state(ProcessState.BACKOFF)
|
6345
6968
|
self.change_state(ProcessState.FATAL)
|
6346
6969
|
|
6347
6970
|
def kill(self, sig: int) -> ta.Optional[str]:
|
@@ -6385,7 +7008,7 @@ class ProcessImpl(Process):
|
|
6385
7008
|
self._killing = True
|
6386
7009
|
self._delay = now + self._config.stopwaitsecs
|
6387
7010
|
# we will already be in the STOPPING state if we're doing a SIGKILL as a result of overrunning stopwaitsecs
|
6388
|
-
self.
|
7011
|
+
self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
|
6389
7012
|
self.change_state(ProcessState.STOPPING)
|
6390
7013
|
|
6391
7014
|
pid = self.pid
|
@@ -6430,7 +7053,7 @@ class ProcessImpl(Process):
|
|
6430
7053
|
|
6431
7054
|
log.debug('sending %s (pid %s) sig %s', process_name, self.pid, sig_name(sig))
|
6432
7055
|
|
6433
|
-
self.
|
7056
|
+
self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
|
6434
7057
|
|
6435
7058
|
try:
|
6436
7059
|
try:
|
@@ -6459,7 +7082,7 @@ class ProcessImpl(Process):
|
|
6459
7082
|
def finish(self, sts: int) -> None:
|
6460
7083
|
"""The process was reaped and we need to report and manage its state."""
|
6461
7084
|
|
6462
|
-
self.drain()
|
7085
|
+
self._dispatchers.drain()
|
6463
7086
|
|
6464
7087
|
es, msg = decode_wait_status(sts)
|
6465
7088
|
|
@@ -6490,7 +7113,7 @@ class ProcessImpl(Process):
|
|
6490
7113
|
self._exitstatus = es
|
6491
7114
|
|
6492
7115
|
fmt, args = 'stopped: %s (%s)', (process_name, msg)
|
6493
|
-
self.
|
7116
|
+
self.check_in_state(ProcessState.STOPPING)
|
6494
7117
|
self.change_state(ProcessState.STOPPED)
|
6495
7118
|
if exit_expected:
|
6496
7119
|
log.info(fmt, *args)
|
@@ -6501,7 +7124,7 @@ class ProcessImpl(Process):
|
|
6501
7124
|
# the program did not stay up long enough to make it to RUNNING implies STARTING -> BACKOFF
|
6502
7125
|
self._exitstatus = None
|
6503
7126
|
self._spawn_err = 'Exited too quickly (process log may have details)'
|
6504
|
-
self.
|
7127
|
+
self.check_in_state(ProcessState.STARTING)
|
6505
7128
|
self.change_state(ProcessState.BACKOFF)
|
6506
7129
|
log.warning('exited: %s (%s)', process_name, msg + '; not expected')
|
6507
7130
|
|
@@ -6517,7 +7140,7 @@ class ProcessImpl(Process):
|
|
6517
7140
|
if self._state == ProcessState.STARTING:
|
6518
7141
|
self.change_state(ProcessState.RUNNING)
|
6519
7142
|
|
6520
|
-
self.
|
7143
|
+
self.check_in_state(ProcessState.RUNNING)
|
6521
7144
|
|
6522
7145
|
if exit_expected:
|
6523
7146
|
# expected exit code
|
@@ -6531,19 +7154,8 @@ class ProcessImpl(Process):
|
|
6531
7154
|
|
6532
7155
|
self._pid = 0
|
6533
7156
|
close_parent_pipes(self._pipes)
|
6534
|
-
self._pipes =
|
6535
|
-
self._dispatchers =
|
6536
|
-
|
6537
|
-
def set_uid(self) -> ta.Optional[str]:
|
6538
|
-
if self._config.uid is None:
|
6539
|
-
return None
|
6540
|
-
msg = drop_privileges(self._config.uid)
|
6541
|
-
return msg
|
6542
|
-
|
6543
|
-
def __repr__(self) -> str:
|
6544
|
-
# repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
|
6545
|
-
name = self._config.name
|
6546
|
-
return f'<Subprocess at {id(self)} with name {name} in state {self.get_state().name}>'
|
7157
|
+
self._pipes = ProcessPipes()
|
7158
|
+
self._dispatchers = Dispatchers([])
|
6547
7159
|
|
6548
7160
|
def get_state(self) -> ProcessState:
|
6549
7161
|
return self._state
|
@@ -6585,7 +7197,7 @@ class ProcessImpl(Process):
|
|
6585
7197
|
# proc.config.startsecs,
|
6586
7198
|
self._delay = 0
|
6587
7199
|
self._backoff = 0
|
6588
|
-
self.
|
7200
|
+
self.check_in_state(ProcessState.STARTING)
|
6589
7201
|
self.change_state(ProcessState.RUNNING)
|
6590
7202
|
msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self._config.startsecs) # noqa
|
6591
7203
|
logger.info('success: %s %s', process_name, msg)
|
@@ -6605,7 +7217,7 @@ class ProcessImpl(Process):
|
|
6605
7217
|
log.warning('killing \'%s\' (%s) with SIGKILL', process_name, self.pid)
|
6606
7218
|
self.kill(signal.SIGKILL)
|
6607
7219
|
|
6608
|
-
def
|
7220
|
+
def after_setuid(self) -> None:
|
6609
7221
|
# temporary logfiles which are erased at start time
|
6610
7222
|
# get_autoname = self.context.get_auto_child_log_name # noqa
|
6611
7223
|
# sid = self.context.config.identifier # noqa
|
@@ -6618,370 +7230,317 @@ class ProcessImpl(Process):
|
|
6618
7230
|
|
6619
7231
|
|
6620
7232
|
########################################
|
6621
|
-
# ../
|
6622
|
-
|
6623
|
-
|
6624
|
-
##
|
6625
|
-
|
7233
|
+
# ../spawningimpl.py
|
6626
7234
|
|
6627
|
-
class SignalHandler:
|
6628
|
-
def __init__(
|
6629
|
-
self,
|
6630
|
-
*,
|
6631
|
-
context: ServerContextImpl,
|
6632
|
-
signal_receiver: SignalReceiver,
|
6633
|
-
process_groups: ProcessGroups,
|
6634
|
-
) -> None:
|
6635
|
-
super().__init__()
|
6636
|
-
|
6637
|
-
self._context = context
|
6638
|
-
self._signal_receiver = signal_receiver
|
6639
|
-
self._process_groups = process_groups
|
6640
|
-
|
6641
|
-
def set_signals(self) -> None:
|
6642
|
-
self._signal_receiver.install(
|
6643
|
-
signal.SIGTERM,
|
6644
|
-
signal.SIGINT,
|
6645
|
-
signal.SIGQUIT,
|
6646
|
-
signal.SIGHUP,
|
6647
|
-
signal.SIGCHLD,
|
6648
|
-
signal.SIGUSR2,
|
6649
|
-
)
|
6650
|
-
|
6651
|
-
def handle_signals(self) -> None:
|
6652
|
-
sig = self._signal_receiver.get_signal()
|
6653
|
-
if not sig:
|
6654
|
-
return
|
6655
|
-
|
6656
|
-
if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
|
6657
|
-
log.warning('received %s indicating exit request', sig_name(sig))
|
6658
|
-
self._context.set_state(SupervisorState.SHUTDOWN)
|
6659
7235
|
|
6660
|
-
|
6661
|
-
|
6662
|
-
log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
|
6663
|
-
else:
|
6664
|
-
log.warning('received %s indicating restart request', sig_name(sig)) # noqa
|
6665
|
-
self._context.set_state(SupervisorState.RESTARTING)
|
7236
|
+
class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], int, OutputDispatcher]):
|
7237
|
+
pass
|
6666
7238
|
|
6667
|
-
elif sig == signal.SIGCHLD:
|
6668
|
-
log.debug('received %s indicating a child quit', sig_name(sig))
|
6669
7239
|
|
6670
|
-
|
6671
|
-
|
7240
|
+
class InputDispatcherFactory(Func3[Process, str, int, InputDispatcher]):
|
7241
|
+
pass
|
6672
7242
|
|
6673
|
-
for group in self._process_groups:
|
6674
|
-
group.reopen_logs()
|
6675
7243
|
|
6676
|
-
|
6677
|
-
log.debug('received %s indicating nothing', sig_name(sig))
|
7244
|
+
InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
|
6678
7245
|
|
6679
7246
|
|
6680
7247
|
##
|
6681
7248
|
|
6682
7249
|
|
6683
|
-
|
6684
|
-
|
6685
|
-
|
6686
|
-
class Supervisor:
|
7250
|
+
class ProcessSpawningImpl(ProcessSpawning):
|
6687
7251
|
def __init__(
|
6688
7252
|
self,
|
7253
|
+
process: Process,
|
6689
7254
|
*,
|
6690
|
-
|
6691
|
-
|
6692
|
-
process_groups: ProcessGroups,
|
6693
|
-
signal_handler: SignalHandler,
|
6694
|
-
event_callbacks: EventCallbacks,
|
6695
|
-
process_group_factory: ProcessGroupFactory,
|
6696
|
-
) -> None:
|
6697
|
-
super().__init__()
|
6698
|
-
|
6699
|
-
self._context = context
|
6700
|
-
self._poller = poller
|
6701
|
-
self._process_groups = process_groups
|
6702
|
-
self._signal_handler = signal_handler
|
6703
|
-
self._event_callbacks = event_callbacks
|
6704
|
-
self._process_group_factory = process_group_factory
|
7255
|
+
server_config: ServerConfig,
|
7256
|
+
pid_history: PidHistory,
|
6705
7257
|
|
6706
|
-
|
6707
|
-
|
6708
|
-
self._stopping = False # set after we detect that we are handling a stop request
|
6709
|
-
self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
|
6710
|
-
|
6711
|
-
#
|
6712
|
-
|
6713
|
-
@property
|
6714
|
-
def context(self) -> ServerContextImpl:
|
6715
|
-
return self._context
|
6716
|
-
|
6717
|
-
def get_state(self) -> SupervisorState:
|
6718
|
-
return self._context.state
|
6719
|
-
|
6720
|
-
#
|
6721
|
-
|
6722
|
-
class DiffToActive(ta.NamedTuple):
|
6723
|
-
added: ta.List[ProcessGroupConfig]
|
6724
|
-
changed: ta.List[ProcessGroupConfig]
|
6725
|
-
removed: ta.List[ProcessGroupConfig]
|
6726
|
-
|
6727
|
-
def diff_to_active(self) -> DiffToActive:
|
6728
|
-
new = self._context.config.groups or []
|
6729
|
-
cur = [group.config for group in self._process_groups]
|
6730
|
-
|
6731
|
-
curdict = dict(zip([cfg.name for cfg in cur], cur))
|
6732
|
-
newdict = dict(zip([cfg.name for cfg in new], new))
|
6733
|
-
|
6734
|
-
added = [cand for cand in new if cand.name not in curdict]
|
6735
|
-
removed = [cand for cand in cur if cand.name not in newdict]
|
6736
|
-
|
6737
|
-
changed = [cand for cand in new if cand != curdict.get(cand.name, cand)]
|
6738
|
-
|
6739
|
-
return Supervisor.DiffToActive(added, changed, removed)
|
6740
|
-
|
6741
|
-
def add_process_group(self, config: ProcessGroupConfig) -> bool:
|
6742
|
-
if self._process_groups.get(config.name) is not None:
|
6743
|
-
return False
|
6744
|
-
|
6745
|
-
group = check_isinstance(self._process_group_factory(config), ProcessGroup)
|
6746
|
-
group.after_setuid()
|
6747
|
-
|
6748
|
-
self._process_groups.add(group)
|
7258
|
+
output_dispatcher_factory: OutputDispatcherFactory,
|
7259
|
+
input_dispatcher_factory: InputDispatcherFactory,
|
6749
7260
|
|
6750
|
-
|
7261
|
+
inherited_fds: ta.Optional[InheritedFds] = None,
|
7262
|
+
) -> None:
|
7263
|
+
super().__init__()
|
6751
7264
|
|
6752
|
-
|
6753
|
-
if self._process_groups[name].get_unstopped_processes():
|
6754
|
-
return False
|
7265
|
+
self._process = process
|
6755
7266
|
|
6756
|
-
self.
|
7267
|
+
self._server_config = server_config
|
7268
|
+
self._pid_history = pid_history
|
6757
7269
|
|
6758
|
-
|
7270
|
+
self._output_dispatcher_factory = output_dispatcher_factory
|
7271
|
+
self._input_dispatcher_factory = input_dispatcher_factory
|
6759
7272
|
|
6760
|
-
|
6761
|
-
process_map: ta.Dict[int, Dispatcher] = {}
|
6762
|
-
for group in self._process_groups:
|
6763
|
-
process_map.update(group.get_dispatchers())
|
6764
|
-
return process_map
|
7273
|
+
self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
|
6765
7274
|
|
6766
|
-
|
6767
|
-
unstopped: ta.List[Process] = []
|
7275
|
+
#
|
6768
7276
|
|
6769
|
-
|
6770
|
-
|
7277
|
+
@property
|
7278
|
+
def process(self) -> Process:
|
7279
|
+
return self._process
|
6771
7280
|
|
6772
|
-
|
6773
|
-
|
6774
|
-
|
6775
|
-
if now > (self._last_shutdown_report + 3): # every 3 secs
|
6776
|
-
names = [as_string(p.config.name) for p in unstopped]
|
6777
|
-
namestr = ', '.join(names)
|
6778
|
-
log.info('waiting for %s to die', namestr)
|
6779
|
-
self._last_shutdown_report = now
|
6780
|
-
for proc in unstopped:
|
6781
|
-
log.debug('%s state: %s', proc.config.name, proc.get_state().name)
|
7281
|
+
@property
|
7282
|
+
def config(self) -> ProcessConfig:
|
7283
|
+
return self._process.config
|
6782
7284
|
|
6783
|
-
|
7285
|
+
@property
|
7286
|
+
def group(self) -> ProcessGroup:
|
7287
|
+
return self._process.group
|
6784
7288
|
|
6785
7289
|
#
|
6786
7290
|
|
6787
|
-
def
|
6788
|
-
|
6789
|
-
|
7291
|
+
def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
|
7292
|
+
try:
|
7293
|
+
exe, argv = self._get_execv_args()
|
7294
|
+
except ProcessError as exc:
|
7295
|
+
raise ProcessSpawnError(exc.args[0]) from exc
|
6790
7296
|
|
6791
|
-
|
6792
|
-
|
6793
|
-
|
6794
|
-
|
6795
|
-
|
7297
|
+
try:
|
7298
|
+
pipes = make_process_pipes(not self.config.redirect_stderr)
|
7299
|
+
except OSError as exc:
|
7300
|
+
code = exc.args[0]
|
7301
|
+
if code == errno.EMFILE:
|
7302
|
+
# too many file descriptors open
|
7303
|
+
msg = f"Too many open files to spawn '{self.process.name}'"
|
7304
|
+
else:
|
7305
|
+
msg = f"Unknown error making pipes for '{self.process.name}': {errno.errorcode.get(code, code)}"
|
7306
|
+
raise ProcessSpawnError(msg) from exc
|
6796
7307
|
|
6797
|
-
|
7308
|
+
try:
|
7309
|
+
dispatchers = self._make_dispatchers(pipes)
|
7310
|
+
except Exception as exc: # noqa
|
7311
|
+
close_pipes(pipes)
|
7312
|
+
raise ProcessSpawnError(f"Unknown error making dispatchers for '{self.process.name}': {exc}") from exc
|
6798
7313
|
|
6799
|
-
|
6800
|
-
|
7314
|
+
try:
|
7315
|
+
pid = os.fork()
|
7316
|
+
except OSError as exc:
|
7317
|
+
code = exc.args[0]
|
7318
|
+
if code == errno.EAGAIN:
|
7319
|
+
# process table full
|
7320
|
+
msg = f"Too many processes in process table to spawn '{self.process.name}'"
|
7321
|
+
else:
|
7322
|
+
msg = f"Unknown error during fork for '{self.process.name}': {errno.errorcode.get(code, code)}"
|
7323
|
+
err = ProcessSpawnError(msg)
|
7324
|
+
close_pipes(pipes)
|
7325
|
+
raise err from exc
|
6801
7326
|
|
6802
|
-
|
6803
|
-
|
6804
|
-
|
6805
|
-
|
7327
|
+
if pid != 0:
|
7328
|
+
sp = SpawnedProcess(
|
7329
|
+
pid,
|
7330
|
+
pipes,
|
7331
|
+
dispatchers,
|
7332
|
+
)
|
7333
|
+
self._spawn_as_parent(sp)
|
7334
|
+
return sp
|
6806
7335
|
|
6807
|
-
|
6808
|
-
self
|
6809
|
-
|
6810
|
-
|
6811
|
-
|
6812
|
-
|
6813
|
-
|
7336
|
+
else:
|
7337
|
+
self._spawn_as_child(
|
7338
|
+
exe,
|
7339
|
+
argv,
|
7340
|
+
pipes,
|
7341
|
+
)
|
7342
|
+
raise RuntimeError('Unreachable') # noqa
|
6814
7343
|
|
6815
|
-
|
7344
|
+
def _get_execv_args(self) -> ta.Tuple[str, ta.Sequence[str]]:
|
7345
|
+
"""
|
7346
|
+
Internal: turn a program name into a file name, using $PATH, make sure it exists / is executable, raising a
|
7347
|
+
ProcessError if not
|
7348
|
+
"""
|
6816
7349
|
|
6817
7350
|
try:
|
6818
|
-
|
6819
|
-
|
7351
|
+
args = shlex.split(self.config.command)
|
7352
|
+
except ValueError as e:
|
7353
|
+
raise BadCommandError(f"Can't parse command {self.config.command!r}: {e}") # noqa
|
6820
7354
|
|
6821
|
-
|
7355
|
+
if args:
|
7356
|
+
program = args[0]
|
7357
|
+
else:
|
7358
|
+
raise BadCommandError('Command is empty')
|
7359
|
+
|
7360
|
+
if '/' in program:
|
7361
|
+
exe = program
|
7362
|
+
try:
|
7363
|
+
st = os.stat(exe)
|
7364
|
+
except OSError:
|
7365
|
+
st = None
|
7366
|
+
|
7367
|
+
else:
|
7368
|
+
path = get_path()
|
7369
|
+
found = None
|
7370
|
+
st = None
|
7371
|
+
for dir in path: # noqa
|
7372
|
+
found = os.path.join(dir, program)
|
7373
|
+
try:
|
7374
|
+
st = os.stat(found)
|
7375
|
+
except OSError:
|
7376
|
+
pass
|
7377
|
+
else:
|
7378
|
+
break
|
6822
7379
|
|
6823
|
-
if
|
6824
|
-
|
7380
|
+
if st is None:
|
7381
|
+
exe = program
|
7382
|
+
else:
|
7383
|
+
exe = found # type: ignore
|
6825
7384
|
|
6826
|
-
|
6827
|
-
|
7385
|
+
# check_execv_args will raise a ProcessError if the execv args are bogus, we break it out into a separate
|
7386
|
+
# options method call here only to service unit tests
|
7387
|
+
check_execv_args(exe, args, st)
|
7388
|
+
|
7389
|
+
return exe, args
|
7390
|
+
|
7391
|
+
def _make_dispatchers(self, pipes: ProcessPipes) -> Dispatchers:
|
7392
|
+
dispatchers: ta.List[Dispatcher] = []
|
7393
|
+
|
7394
|
+
if pipes.stdout is not None:
|
7395
|
+
dispatchers.append(check_isinstance(self._output_dispatcher_factory(
|
7396
|
+
self.process,
|
7397
|
+
ProcessCommunicationStdoutEvent,
|
7398
|
+
pipes.stdout,
|
7399
|
+
), OutputDispatcher))
|
7400
|
+
|
7401
|
+
if pipes.stderr is not None:
|
7402
|
+
dispatchers.append(check_isinstance(self._output_dispatcher_factory(
|
7403
|
+
self.process,
|
7404
|
+
ProcessCommunicationStderrEvent,
|
7405
|
+
pipes.stderr,
|
7406
|
+
), OutputDispatcher))
|
7407
|
+
|
7408
|
+
if pipes.stdin is not None:
|
7409
|
+
dispatchers.append(check_isinstance(self._input_dispatcher_factory(
|
7410
|
+
self.process,
|
7411
|
+
'stdin',
|
7412
|
+
pipes.stdin,
|
7413
|
+
), InputDispatcher))
|
6828
7414
|
|
6829
|
-
|
7415
|
+
return Dispatchers(dispatchers)
|
6830
7416
|
|
6831
|
-
|
6832
|
-
if callback is not None and not callback(self):
|
6833
|
-
break
|
7417
|
+
#
|
6834
7418
|
|
6835
|
-
|
7419
|
+
def _spawn_as_parent(self, sp: SpawnedProcess) -> None:
|
7420
|
+
close_child_pipes(sp.pipes)
|
6836
7421
|
|
6837
|
-
|
6838
|
-
self._context.cleanup()
|
7422
|
+
self._pid_history[sp.pid] = self.process
|
6839
7423
|
|
6840
7424
|
#
|
6841
7425
|
|
6842
|
-
def
|
6843
|
-
|
6844
|
-
|
6845
|
-
|
6846
|
-
|
7426
|
+
def _spawn_as_child(
|
7427
|
+
self,
|
7428
|
+
exe: str,
|
7429
|
+
argv: ta.Sequence[str],
|
7430
|
+
pipes: ProcessPipes,
|
7431
|
+
) -> ta.NoReturn:
|
7432
|
+
try:
|
7433
|
+
# Prevent child from receiving signals sent to the parent by calling os.setpgrp to create a new process
|
7434
|
+
# group for the child. This prevents, for instance, the case of child processes being sent a SIGINT when
|
7435
|
+
# running supervisor in foreground mode and Ctrl-C in the terminal window running supervisord is pressed.
|
7436
|
+
# Presumably it also prevents HUP, etc. received by supervisord from being sent to children.
|
7437
|
+
os.setpgrp()
|
6847
7438
|
|
6848
|
-
|
6849
|
-
self._ordered_stop_groups_phase_2()
|
7439
|
+
#
|
6850
7440
|
|
6851
|
-
|
6852
|
-
|
6853
|
-
# stop the last group (the one with the "highest" priority)
|
6854
|
-
self._stop_groups[-1].stop_all()
|
7441
|
+
# After preparation sending to fd 2 will put this output in the stderr log.
|
7442
|
+
self._prepare_child_fds(pipes)
|
6855
7443
|
|
6856
|
-
|
6857
|
-
# after phase 1 we've transitioned and reaped, let's see if we can remove the group we stopped from the
|
6858
|
-
# stop_groups queue.
|
6859
|
-
if self._stop_groups:
|
6860
|
-
# pop the last group (the one with the "highest" priority)
|
6861
|
-
group = self._stop_groups.pop()
|
6862
|
-
if group.get_unstopped_processes():
|
6863
|
-
# if any processes in the group aren't yet in a stopped state, we're not yet done shutting this group
|
6864
|
-
# down, so push it back on to the end of the stop group queue
|
6865
|
-
self._stop_groups.append(group)
|
7444
|
+
#
|
6866
7445
|
|
6867
|
-
|
6868
|
-
|
6869
|
-
|
7446
|
+
setuid_msg = self._set_uid()
|
7447
|
+
if setuid_msg:
|
7448
|
+
uid = self.config.uid
|
7449
|
+
msg = f"Couldn't setuid to {uid}: {setuid_msg}\n"
|
7450
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
7451
|
+
raise RuntimeError(msg)
|
6870
7452
|
|
6871
|
-
|
6872
|
-
pgroups.sort()
|
7453
|
+
#
|
6873
7454
|
|
6874
|
-
|
6875
|
-
|
6876
|
-
|
6877
|
-
|
6878
|
-
|
6879
|
-
|
7455
|
+
env = os.environ.copy()
|
7456
|
+
env['SUPERVISOR_ENABLED'] = '1'
|
7457
|
+
env['SUPERVISOR_PROCESS_NAME'] = self.process.name
|
7458
|
+
if self.group:
|
7459
|
+
env['SUPERVISOR_GROUP_NAME'] = self.group.name
|
7460
|
+
if self.config.environment is not None:
|
7461
|
+
env.update(self.config.environment)
|
6880
7462
|
|
6881
|
-
|
7463
|
+
#
|
6882
7464
|
|
6883
|
-
|
6884
|
-
|
6885
|
-
|
7465
|
+
cwd = self.config.directory
|
7466
|
+
try:
|
7467
|
+
if cwd is not None:
|
7468
|
+
os.chdir(os.path.expanduser(cwd))
|
7469
|
+
except OSError as exc:
|
7470
|
+
code = errno.errorcode.get(exc.args[0], exc.args[0])
|
7471
|
+
msg = f"Couldn't chdir to {cwd}: {code}\n"
|
7472
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
7473
|
+
raise RuntimeError(msg) from exc
|
6886
7474
|
|
6887
|
-
|
6888
|
-
if dispatcher.readable():
|
6889
|
-
self._poller.register_readable(fd)
|
6890
|
-
if dispatcher.writable():
|
6891
|
-
self._poller.register_writable(fd)
|
7475
|
+
#
|
6892
7476
|
|
6893
|
-
|
6894
|
-
|
7477
|
+
try:
|
7478
|
+
if self.config.umask is not None:
|
7479
|
+
os.umask(self.config.umask)
|
7480
|
+
os.execve(exe, list(argv), env)
|
6895
7481
|
|
6896
|
-
|
6897
|
-
|
6898
|
-
|
6899
|
-
|
6900
|
-
log.debug('read event caused by %r', dispatcher)
|
6901
|
-
dispatcher.handle_read_event()
|
6902
|
-
if not dispatcher.readable():
|
6903
|
-
self._poller.unregister_readable(fd)
|
6904
|
-
except ExitNow:
|
6905
|
-
raise
|
6906
|
-
except Exception: # noqa
|
6907
|
-
combined_map[fd].handle_error()
|
6908
|
-
else:
|
6909
|
-
# if the fd is not in combined_map, we should unregister it. otherwise, it will be polled every
|
6910
|
-
# time, which may cause 100% cpu usage
|
6911
|
-
log.debug('unexpected read event from fd %r', fd)
|
6912
|
-
try:
|
6913
|
-
self._poller.unregister_readable(fd)
|
6914
|
-
except Exception: # noqa
|
6915
|
-
pass
|
7482
|
+
except OSError as exc:
|
7483
|
+
code = errno.errorcode.get(exc.args[0], exc.args[0])
|
7484
|
+
msg = f"Couldn't exec {argv[0]}: {code}\n"
|
7485
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
6916
7486
|
|
6917
|
-
|
6918
|
-
|
6919
|
-
|
6920
|
-
|
6921
|
-
log.debug('write event caused by %r', dispatcher)
|
6922
|
-
dispatcher.handle_write_event()
|
6923
|
-
if not dispatcher.writable():
|
6924
|
-
self._poller.unregister_writable(fd)
|
6925
|
-
except ExitNow:
|
6926
|
-
raise
|
6927
|
-
except Exception: # noqa
|
6928
|
-
combined_map[fd].handle_error()
|
6929
|
-
else:
|
6930
|
-
log.debug('unexpected write event from fd %r', fd)
|
6931
|
-
try:
|
6932
|
-
self._poller.unregister_writable(fd)
|
6933
|
-
except Exception: # noqa
|
6934
|
-
pass
|
7487
|
+
except Exception: # noqa
|
7488
|
+
(file, fun, line), t, v, tb = compact_traceback()
|
7489
|
+
msg = f"Couldn't exec {exe}: {t}, {v}: file: {file} line: {line}\n"
|
7490
|
+
os.write(2, as_bytes('supervisor: ' + msg))
|
6935
7491
|
|
6936
|
-
|
6937
|
-
|
7492
|
+
finally:
|
7493
|
+
os.write(2, as_bytes('supervisor: child process was not spawned\n'))
|
7494
|
+
real_exit(127) # exit process with code for spawn failure
|
6938
7495
|
|
6939
|
-
|
6940
|
-
if depth >= 100:
|
6941
|
-
return
|
7496
|
+
raise RuntimeError('Unreachable')
|
6942
7497
|
|
6943
|
-
|
6944
|
-
|
6945
|
-
return
|
7498
|
+
def _prepare_child_fds(self, pipes: ProcessPipes) -> None:
|
7499
|
+
os.dup2(check_not_none(pipes.child_stdin), 0)
|
6946
7500
|
|
6947
|
-
|
6948
|
-
|
6949
|
-
|
6950
|
-
|
7501
|
+
os.dup2(check_not_none(pipes.child_stdout), 1)
|
7502
|
+
|
7503
|
+
if self.config.redirect_stderr:
|
7504
|
+
os.dup2(check_not_none(pipes.child_stdout), 2)
|
6951
7505
|
else:
|
6952
|
-
|
6953
|
-
del self._context.pid_history[pid]
|
7506
|
+
os.dup2(check_not_none(pipes.child_stderr), 2)
|
6954
7507
|
|
6955
|
-
|
6956
|
-
|
6957
|
-
|
7508
|
+
for i in range(3, self._server_config.minfds):
|
7509
|
+
if i in self._inherited_fds:
|
7510
|
+
continue
|
7511
|
+
close_fd(i)
|
6958
7512
|
|
6959
|
-
def
|
6960
|
-
|
7513
|
+
def _set_uid(self) -> ta.Optional[str]:
|
7514
|
+
if self.config.uid is None:
|
7515
|
+
return None
|
6961
7516
|
|
6962
|
-
|
6963
|
-
|
6964
|
-
now = time.time()
|
7517
|
+
msg = drop_privileges(self.config.uid)
|
7518
|
+
return msg
|
6965
7519
|
|
6966
|
-
for event in TICK_EVENTS:
|
6967
|
-
period = event.period
|
6968
7520
|
|
6969
|
-
|
6970
|
-
if last_tick is None:
|
6971
|
-
# we just started up
|
6972
|
-
last_tick = self._ticks[period] = timeslice(period, now)
|
7521
|
+
##
|
6973
7522
|
|
6974
|
-
this_tick = timeslice(period, now)
|
6975
|
-
if this_tick != last_tick:
|
6976
|
-
self._ticks[period] = this_tick
|
6977
|
-
self._event_callbacks.notify(event(this_tick, self))
|
6978
7523
|
|
7524
|
+
def check_execv_args(
|
7525
|
+
exe: str,
|
7526
|
+
argv: ta.Sequence[str],
|
7527
|
+
st: ta.Optional[os.stat_result],
|
7528
|
+
) -> None:
|
7529
|
+
if st is None:
|
7530
|
+
raise NotFoundError(f"Can't find command {exe!r}")
|
6979
7531
|
|
6980
|
-
|
6981
|
-
|
7532
|
+
elif stat.S_ISDIR(st[stat.ST_MODE]):
|
7533
|
+
raise NotExecutableError(f'Command at {exe!r} is a directory')
|
6982
7534
|
|
7535
|
+
elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
|
7536
|
+
raise NotExecutableError(f'Command at {exe!r} is not executable')
|
6983
7537
|
|
6984
|
-
|
7538
|
+
elif not os.access(exe, os.X_OK):
|
7539
|
+
raise NoPermissionError(f'No permission to run command {exe!r}')
|
7540
|
+
|
7541
|
+
|
7542
|
+
########################################
|
7543
|
+
# ../inject.py
|
6985
7544
|
|
6986
7545
|
|
6987
7546
|
def bind_server(
|
@@ -6993,7 +7552,12 @@ def bind_server(
|
|
6993
7552
|
lst: ta.List[InjectorBindingOrBindings] = [
|
6994
7553
|
inj.bind(config),
|
6995
7554
|
|
6996
|
-
inj.
|
7555
|
+
inj.bind_array_type(DaemonizeListener, DaemonizeListeners),
|
7556
|
+
|
7557
|
+
inj.bind(SupervisorSetupImpl, singleton=True),
|
7558
|
+
inj.bind(SupervisorSetup, to_key=SupervisorSetupImpl),
|
7559
|
+
|
7560
|
+
inj.bind(DaemonizeListener, array=True, to_key=Poller),
|
6997
7561
|
|
6998
7562
|
inj.bind(ServerContextImpl, singleton=True),
|
6999
7563
|
inj.bind(ServerContext, to_key=ServerContextImpl),
|
@@ -7003,14 +7567,18 @@ def bind_server(
|
|
7003
7567
|
inj.bind(SignalReceiver, singleton=True),
|
7004
7568
|
|
7005
7569
|
inj.bind(SignalHandler, singleton=True),
|
7006
|
-
inj.bind(
|
7570
|
+
inj.bind(ProcessGroupManager, singleton=True),
|
7007
7571
|
inj.bind(Supervisor, singleton=True),
|
7008
7572
|
|
7009
|
-
inj.
|
7010
|
-
inj.bind_factory(ProcessFactory, ProcessImpl),
|
7573
|
+
inj.bind(PidHistory()),
|
7011
7574
|
|
7012
|
-
inj.bind_factory(
|
7013
|
-
inj.bind_factory(
|
7575
|
+
inj.bind_factory(ProcessGroupImpl, ProcessGroupFactory),
|
7576
|
+
inj.bind_factory(ProcessImpl, ProcessFactory),
|
7577
|
+
|
7578
|
+
inj.bind_factory(ProcessSpawningImpl, ProcessSpawningFactory),
|
7579
|
+
|
7580
|
+
inj.bind_factory(OutputDispatcherImpl, OutputDispatcherFactory),
|
7581
|
+
inj.bind_factory(InputDispatcherImpl, InputDispatcherFactory),
|
7014
7582
|
]
|
7015
7583
|
|
7016
7584
|
#
|
@@ -7022,6 +7590,16 @@ def bind_server(
|
|
7022
7590
|
|
7023
7591
|
#
|
7024
7592
|
|
7593
|
+
if config.user is not None:
|
7594
|
+
user = get_user(config.user)
|
7595
|
+
lst.append(inj.bind(user, key=SupervisorUser))
|
7596
|
+
|
7597
|
+
#
|
7598
|
+
|
7599
|
+
lst.append(inj.bind(get_poller_impl(), key=Poller, singleton=True))
|
7600
|
+
|
7601
|
+
#
|
7602
|
+
|
7025
7603
|
return inj.as_bindings(*lst)
|
7026
7604
|
|
7027
7605
|
|