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.
Files changed (34) hide show
  1. ominfra/clouds/aws/auth.py +1 -1
  2. ominfra/deploy/_executor.py +1 -1
  3. ominfra/deploy/poly/_main.py +1 -1
  4. ominfra/pyremote/_runcommands.py +1 -1
  5. ominfra/scripts/journald2aws.py +2 -2
  6. ominfra/scripts/supervisor.py +1796 -1218
  7. ominfra/supervisor/collections.py +52 -0
  8. ominfra/supervisor/context.py +2 -336
  9. ominfra/supervisor/datatypes.py +1 -63
  10. ominfra/supervisor/dispatchers.py +20 -324
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -111
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +44 -19
  15. ominfra/supervisor/main.py +1 -1
  16. ominfra/supervisor/pipes.py +83 -0
  17. ominfra/supervisor/poller.py +6 -3
  18. ominfra/supervisor/privileges.py +65 -0
  19. ominfra/supervisor/processes.py +18 -0
  20. ominfra/supervisor/{process.py → processesimpl.py} +96 -330
  21. ominfra/supervisor/setup.py +38 -0
  22. ominfra/supervisor/setupimpl.py +261 -0
  23. ominfra/supervisor/signals.py +24 -16
  24. ominfra/supervisor/spawning.py +31 -0
  25. ominfra/supervisor/spawningimpl.py +347 -0
  26. ominfra/supervisor/supervisor.py +52 -77
  27. ominfra/supervisor/types.py +101 -45
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
@@ -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
- def name_to_uid(name: str) -> int:
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
- _SIG_NAMES: ta.Optional[ta.Mapping[int, str]] = None
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 sig_name(sig: int) -> str:
1158
- global _SIG_NAMES
1159
- if _SIG_NAMES is None:
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
- def _init_sig_names() -> ta.Dict[int, str]:
1165
- d = {}
1166
- for k, v in signal.__dict__.items(): # noqa
1167
- k_startswith = getattr(k, 'startswith', None)
1168
- if k_startswith is None:
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 Func(ta.Generic[T]):
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
- # ../utils.py
2035
+ # ../setup.py
1864
2036
 
1865
2037
 
1866
2038
  ##
1867
2039
 
1868
2040
 
1869
- def as_bytes(s: ta.Union[str, bytes], encoding: str = 'utf8') -> bytes:
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
- def as_string(s: ta.Union[str, bytes], encoding: str = 'utf8') -> str:
1877
- if isinstance(s, str):
1878
- return s
1879
- else:
1880
- return s.decode(encoding)
2044
+ ##
1881
2045
 
1882
2046
 
1883
- def find_prefix_at_end(haystack: bytes, needle: bytes) -> int:
1884
- l = len(needle) - 1
1885
- while l and not haystack.endswith(needle[:l]):
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
- def compact_traceback() -> ta.Tuple[
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(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
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(self, obj: ta.Any) -> ta.Any:
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(Exception):
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
- uw = obj
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(obj),
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[ta.Any]] = None,
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
- seen: ta.Set[InjectorKey] = set(map(as_injector_key, skip_kwargs)) if skip_kwargs is not None else set()
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 list(insp.signature.parameters.values())[skip_args:]:
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(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
2855
- kt = build_injection_kwargs_target(obj)
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(self, obj: ta.Any) -> ta.Any:
2868
- kws = self.provide_kwargs(obj)
2869
- return obj(**kws)
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
- factory_cls: ta.Any,
3009
- factory_fn: ta.Callable[..., T],
3010
- ) -> ta.Callable[..., Func[T]]:
3011
- def outer(injector: Injector) -> factory_cls:
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(functools.partial(factory_fn, *args, **kwargs))
3014
- return Func(inner)
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, p: ta.Optional[Injector] = None) -> Injector:
3053
- return _Injector(as_injector_bindings(*args), p)
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
- factory_cls: ta.Any,
3094
- factory_fn: ta.Callable[..., T],
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(make_injector_factory(factory_cls, factory_fn))
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 KqueuePoller is not None
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 readable(self) -> bool:
5366
+ def process(self) -> 'Process':
4904
5367
  raise NotImplementedError
4905
5368
 
5369
+ @property
4906
5370
  @abc.abstractmethod
4907
- def writable(self) -> bool:
5371
+ def channel(self) -> str:
4908
5372
  raise NotImplementedError
4909
5373
 
4910
- def handle_read_event(self) -> None:
4911
- raise TypeError
4912
-
4913
- def handle_write_event(self) -> None:
4914
- raise TypeError
4915
-
5374
+ @property
4916
5375
  @abc.abstractmethod
4917
- def handle_error(self) -> None:
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 write(self, chars: ta.Union[bytes, str]) -> None:
5387
+ def close(self) -> None:
4933
5388
  raise NotImplementedError
4934
5389
 
4935
5390
  @abc.abstractmethod
4936
- def flush(self) -> None:
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 pid(self) -> int:
5397
+ def readable(self) -> bool:
4945
5398
  raise NotImplementedError
4946
5399
 
4947
- @property
4948
5400
  @abc.abstractmethod
4949
- def config(self) -> ProcessConfig:
5401
+ def writable(self) -> bool:
4950
5402
  raise NotImplementedError
4951
5403
 
4952
- def __lt__(self, other):
4953
- return self.config.priority < other.config.priority
5404
+ #
4954
5405
 
4955
- def __eq__(self, other):
4956
- return self.config.priority == other.config.priority
5406
+ def handle_read_event(self) -> None:
5407
+ raise TypeError
4957
5408
 
4958
- @property
4959
- @abc.abstractmethod
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 give_up(self) -> None:
5425
+ def write(self, chars: ta.Union[bytes, str]) -> None:
4981
5426
  raise NotImplementedError
4982
5427
 
4983
5428
  @abc.abstractmethod
4984
- def transition(self) -> None:
5429
+ def flush(self) -> None:
4985
5430
  raise NotImplementedError
4986
5431
 
4987
- @abc.abstractmethod
4988
- def get_state(self) -> ProcessState:
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 create_auto_child_logs(self) -> None:
5444
+ def config(self) -> ProcessConfig:
4993
5445
  raise NotImplementedError
4994
5446
 
5447
+ @property
4995
5448
  @abc.abstractmethod
4996
- def get_dispatchers(self) -> ta.Mapping[int, Dispatcher]:
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 config(self) -> ProcessGroupConfig:
5461
+ def context(self) -> ServerContext:
5005
5462
  raise NotImplementedError
5006
5463
 
5007
- def __lt__(self, other):
5008
- return self.config.priority < other.config.priority
5464
+ @abc.abstractmethod
5465
+ def finish(self, sts: int) -> None:
5466
+ raise NotImplementedError
5009
5467
 
5010
- def __eq__(self, other):
5011
- return self.config.priority == other.config.priority
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 stop_all(self) -> None:
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 before_remove(self) -> None:
5508
+ def config(self) -> ProcessGroupConfig:
5028
5509
  raise NotImplementedError
5029
5510
 
5511
+ @property
5030
5512
  @abc.abstractmethod
5031
- def get_dispatchers(self) -> ta.Mapping[int, Dispatcher]:
5513
+ def by_name(self) -> ta.Mapping[str, Process]:
5032
5514
  raise NotImplementedError
5033
5515
 
5516
+ #
5517
+
5034
5518
  @abc.abstractmethod
5035
- def reopen_logs(self) -> None:
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 after_setuid(self) -> None:
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
- @property
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 set_uid_or_exit(self) -> None:
5133
- """
5134
- Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
5135
- process via usage() if privileges could not be dropped.
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
- def set_rlimits_or_exit(self) -> None:
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
- if hasattr(resource, 'RLIMIT_NOFILE'):
5161
- limits.append({
5162
- 'msg': (
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
- if hasattr(resource, 'RLIMIT_NPROC'):
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
- for limit in limits:
5189
- min_limit = limit['min']
5190
- res = limit['resource']
5191
- msg = limit['msg']
5192
- name = limit['name']
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
- soft, hard = resource.getrlimit(res) # type: ignore
5624
+ #
5195
5625
 
5196
- # -1 means unlimited
5197
- if soft < min_limit and soft != -1: # type: ignore
5198
- if hard < min_limit and hard != -1: # type: ignore
5199
- # setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
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
- try:
5204
- resource.setrlimit(res, (min_limit, hard)) # type: ignore
5205
- log.info('Increased %s limit to %s', name, min_limit)
5206
- except (resource.error, ValueError):
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
- def cleanup_fds(self) -> None:
5221
- # try to close any leaked file descriptors (for reload)
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
- for filename in filenames:
5236
- if fnre.match(filename):
5237
- pathname = os.path.join(child_logdir, filename)
5238
- try:
5239
- os.remove(pathname)
5240
- except OSError:
5241
- log.warning('Failed to clean up %r', pathname)
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
- def daemonize(self) -> None:
5244
- self._poller.before_daemonize()
5245
- self._daemonize()
5246
- self._poller.after_daemonize()
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
- def _daemonize(self) -> None:
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
- pid = os.fork()
5265
- if pid != 0:
5266
- # Parent
5267
- log.debug('supervisord forked; parent exiting')
5268
- real_exit(0)
5659
+ #
5269
5660
 
5270
- # Child
5271
- log.info('daemonizing the supervisord process')
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
- os.dup2(0, os.open('/dev/null', os.O_RDONLY))
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
- os.setsid()
5666
+ @property
5667
+ def process(self) -> Process:
5668
+ return self._process
5285
5669
 
5286
- os.umask(self.config.umask)
5670
+ @property
5671
+ def channel(self) -> str:
5672
+ return self._channel
5287
5673
 
5288
- # XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
5289
- # file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
5290
- # again after the setsid() call, for obscure SVR4 reasons.
5674
+ @property
5675
+ def fd(self) -> int:
5676
+ return self._fd
5291
5677
 
5292
- def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
5293
- prefix = f'{name}-{channel}---{identifier}-'
5294
- logfile = mktempfile(
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
- def write_pidfile(self) -> None:
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
- def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
5314
- """
5315
- Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
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
- ProcessFactory = ta.NewType('ProcessFactory', Func[Process]) # (config: ProcessConfig, group: ProcessGroup)
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
- self._processes = {}
6053
+ by_name: ta.Dict[str, Process] = {}
5783
6054
  for pconfig in self._config.processes or []:
5784
- process = check_isinstance(self._process_factory(pconfig, self), Process)
5785
- self._processes[pconfig.name] = process
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 config(self) -> ProcessGroupConfig:
5789
- return self._config
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 context(self) -> ServerContext:
5797
- return self._context
6077
+ def config(self) -> ProcessGroupConfig:
6078
+ return self._config
5798
6079
 
5799
- def __repr__(self):
5800
- # repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
5801
- name = self._config.name
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
- def remove_logs(self) -> None:
5805
- for process in self._processes.values():
5806
- process.remove_logs()
6084
+ #
5807
6085
 
5808
- def reopen_logs(self) -> None:
5809
- for process in self._processes.values():
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._processes.values())
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
- def after_setuid(self) -> None:
5848
- for proc in self._processes.values():
5849
- proc.create_auto_child_logs()
6112
+ ########################################
6113
+ # ../processes.py
5850
6114
 
5851
6115
 
5852
6116
  ##
5853
6117
 
5854
6118
 
5855
- class ProcessGroups:
5856
- def __init__(
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
- self._by_name: ta.Dict[str, ProcessGroup] = {}
6123
+ ##
5866
6124
 
5867
- def get(self, name: str) -> ta.Optional[ProcessGroup]:
5868
- return self._by_name.get(name)
5869
6125
 
5870
- def __getitem__(self, name: str) -> ProcessGroup:
5871
- return self._by_name[name]
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
- def __iter__(self) -> ta.Iterator[ProcessGroup]:
5877
- return iter(self._by_name.values())
6130
+ ########################################
6131
+ # ../setupimpl.py
5878
6132
 
5879
- def all(self) -> ta.Mapping[str, ProcessGroup]:
5880
- return self._by_name
5881
6133
 
5882
- def add(self, group: ProcessGroup) -> None:
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
- self._event_callbacks.notify(ProcessGroupAddedEvent(name))
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
- def remove(self, name: str) -> None:
5891
- group = self._by_name[name]
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
- group.before_remove()
6526
+ def remove_process_group(self, name: str) -> bool:
6527
+ if self._process_groups[name].get_unstopped_processes():
6528
+ return False
5894
6529
 
5895
- del self._by_name[name]
6530
+ self._process_groups.remove(name)
5896
6531
 
5897
- self._event_callbacks.notify(ProcessGroupRemovedEvent(name))
6532
+ return True
5898
6533
 
5899
- def clear(self) -> None:
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
- # ../process.py
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
- # (process: Process, event_type: ta.Type[ProcessCommunicationEvent], fd: int)
5909
- OutputDispatcherFactory = ta.NewType('OutputDispatcherFactory', Func[OutputDispatcher])
6553
+ return unstopped
5910
6554
 
5911
- # (process: Process, event_type: ta.Type[ProcessCommunicationEvent], fd: int)
5912
- InputDispatcherFactory = ta.NewType('InputDispatcherFactory', Func[InputDispatcher])
6555
+ #
5913
6556
 
5914
- InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
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._output_dispatcher_factory = output_dispatcher_factory
5946
- self._input_dispatcher_factory = input_dispatcher_factory
6769
+ self._spawning = process_spawning_factory(self)
5947
6770
 
5948
- self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
6771
+ #
5949
6772
 
5950
- self._dispatchers: ta.Dict[int, Dispatcher] = {}
5951
- self._pipes: ta.Dict[str, int] = {}
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
- @property
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
- if commandargs:
6044
- program = commandargs[0]
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
- if '/' in program:
6049
- filename = program
6050
- try:
6051
- st = os.stat(filename)
6052
- except OSError:
6053
- st = None
6799
+ #
6054
6800
 
6055
- else:
6056
- path = get_path()
6057
- found = None
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
- # check_execv_args will raise a ProcessError if the execv args are bogus, we break it out into a separate
6073
- # options method call here only to service unit tests
6074
- check_execv_args(filename, commandargs, st)
6805
+ @property
6806
+ def config(self) -> ProcessConfig:
6807
+ return self._config
6075
6808
 
6076
- return filename, commandargs
6809
+ @property
6810
+ def group(self) -> ProcessGroup:
6811
+ return self._group
6077
6812
 
6078
- def change_state(self, new_state: ProcessState, expected: bool = True) -> bool:
6079
- old_state = self._state
6080
- if new_state is old_state:
6081
- return False
6813
+ @property
6814
+ def pid(self) -> int:
6815
+ return self._pid
6082
6816
 
6083
- self._state = new_state
6084
- if new_state == ProcessState.BACKOFF:
6085
- now = time.time()
6086
- self._backoff += 1
6087
- self._delay = now + self._backoff
6817
+ #
6088
6818
 
6089
- event_class = PROCESS_STATE_EVENT_MAP.get(new_state)
6090
- if event_class is not None:
6091
- event = event_class(self, old_state, expected)
6092
- self._event_callbacks.notify(event)
6819
+ @property
6820
+ def context(self) -> ServerContext:
6821
+ return self._context
6093
6822
 
6094
- return True
6823
+ @property
6824
+ def state(self) -> ProcessState:
6825
+ return self._state
6095
6826
 
6096
- def _check_in_state(self, *states: ProcessState) -> None:
6097
- if self._state not in states:
6098
- current_state = self._state.name
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
- def _record_spawn_err(self, msg: str) -> None:
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
- filename, argv = self._get_execv_args()
6133
- except ProcessError as what:
6134
- self._record_spawn_err(what.args[0])
6135
- self._check_in_state(ProcessState.STARTING)
6136
- self.change_state(ProcessState.BACKOFF)
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
- if pid != 0:
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
- def _make_dispatchers(self) -> ta.Tuple[ta.Mapping[int, Dispatcher], ta.Mapping[str, int]]:
6177
- use_stderr = not self._config.redirect_stderr
6868
+ self._pid = sp.pid
6869
+ self._pipes = sp.pipes
6870
+ self._dispatchers = sp.dispatchers
6178
6871
 
6179
- p = make_pipes(use_stderr)
6180
- stdout_fd, stderr_fd, stdin_fd = p['stdout'], p['stderr'], p['stdin']
6872
+ self._delay = time.time() + self.config.startsecs
6181
6873
 
6182
- dispatchers: ta.Dict[int, Dispatcher] = {}
6874
+ return sp.pid
6183
6875
 
6184
- dispatcher_kw = dict(
6185
- event_callbacks=self._event_callbacks,
6186
- )
6876
+ def get_dispatchers(self) -> Dispatchers:
6877
+ return self._dispatchers
6187
6878
 
6188
- etype: ta.Type[ProcessCommunicationEvent]
6189
- if stdout_fd is not None:
6190
- etype = ProcessCommunicationStdoutEvent
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
- return dispatchers, p
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
- def _spawn_as_parent(self, pid: int) -> int:
6218
- # Parent
6219
- self._pid = pid
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
- for i in range(3, self.context.config.minfds):
6236
- if i in self._inherited_fds:
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
- def _spawn_as_child(self, filename: str, argv: ta.Sequence[str]) -> None:
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
- self._prepare_child_fds()
6249
- # sending to fd 2 will put this output in the stderr log
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
- # set user
6252
- setuid_msg = self.set_uid()
6253
- if setuid_msg:
6254
- uid = self._config.uid
6255
- msg = f"couldn't setuid to {uid}: {setuid_msg}\n"
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
- # set environment
6260
- env = os.environ.copy()
6261
- env['SUPERVISOR_ENABLED'] = '1'
6262
- env['SUPERVISOR_PROCESS_NAME'] = self._config.name
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
- # set umask, then execve
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
- # this point should only be reached if execve failed. the finally clause will exit the child process.
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
- finally:
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._check_in_state(ProcessState.BACKOFF)
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._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
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._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
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._check_in_state(ProcessState.STOPPING)
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._check_in_state(ProcessState.STARTING)
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._check_in_state(ProcessState.RUNNING)
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._check_in_state(ProcessState.STARTING)
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 create_auto_child_logs(self) -> None:
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
- # ../supervisor.py
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
- elif sig == signal.SIGHUP:
6661
- if self._context.state == SupervisorState.SHUTDOWN:
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
- elif sig == signal.SIGUSR2:
6671
- log.info('received %s indicating log reopen request', sig_name(sig))
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
- else:
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
- ProcessGroupFactory = ta.NewType('ProcessGroupFactory', Func[ProcessGroup]) # (config: ProcessGroupConfig)
6684
-
6685
-
6686
- class Supervisor:
7250
+ class ProcessSpawningImpl(ProcessSpawning):
6687
7251
  def __init__(
6688
7252
  self,
7253
+ process: Process,
6689
7254
  *,
6690
- context: ServerContextImpl,
6691
- poller: Poller,
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
- self._ticks: ta.Dict[int, float] = {}
6707
- self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
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
- return True
7261
+ inherited_fds: ta.Optional[InheritedFds] = None,
7262
+ ) -> None:
7263
+ super().__init__()
6751
7264
 
6752
- def remove_process_group(self, name: str) -> bool:
6753
- if self._process_groups[name].get_unstopped_processes():
6754
- return False
7265
+ self._process = process
6755
7266
 
6756
- self._process_groups.remove(name)
7267
+ self._server_config = server_config
7268
+ self._pid_history = pid_history
6757
7269
 
6758
- return True
7270
+ self._output_dispatcher_factory = output_dispatcher_factory
7271
+ self._input_dispatcher_factory = input_dispatcher_factory
6759
7272
 
6760
- def get_process_map(self) -> ta.Dict[int, Dispatcher]:
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
- def shutdown_report(self) -> ta.List[Process]:
6767
- unstopped: ta.List[Process] = []
7275
+ #
6768
7276
 
6769
- for group in self._process_groups:
6770
- unstopped.extend(group.get_unstopped_processes())
7277
+ @property
7278
+ def process(self) -> Process:
7279
+ return self._process
6771
7280
 
6772
- if unstopped:
6773
- # throttle 'waiting for x to die' reports
6774
- now = time.time()
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
- return unstopped
7285
+ @property
7286
+ def group(self) -> ProcessGroup:
7287
+ return self._process.group
6784
7288
 
6785
7289
  #
6786
7290
 
6787
- def main(self) -> None:
6788
- self.setup()
6789
- self.run()
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
- @cached_nullary
6792
- def setup(self) -> None:
6793
- if not self._context.first:
6794
- # prevent crash on libdispatch-based systems, at least for the first request
6795
- self._context.cleanup_fds()
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
- self._context.set_uid_or_exit()
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
- if self._context.first:
6800
- self._context.set_rlimits_or_exit()
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
- # this sets the options.logger object delay logger instantiation until after setuid
6803
- if not self._context.config.nocleanup:
6804
- # clean up old automatic logs
6805
- self._context.clear_auto_child_logdir()
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
- def run(
6808
- self,
6809
- *,
6810
- callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
6811
- ) -> None:
6812
- self._process_groups.clear()
6813
- self._stop_groups = None # clear
7336
+ else:
7337
+ self._spawn_as_child(
7338
+ exe,
7339
+ argv,
7340
+ pipes,
7341
+ )
7342
+ raise RuntimeError('Unreachable') # noqa
6814
7343
 
6815
- self._event_callbacks.clear()
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
- for config in self._context.config.groups or []:
6819
- self.add_process_group(config)
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
- self._signal_handler.set_signals()
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 not self._context.config.nodaemon and self._context.first:
6824
- self._context.daemonize()
7380
+ if st is None:
7381
+ exe = program
7382
+ else:
7383
+ exe = found # type: ignore
6825
7384
 
6826
- # writing pid file needs to come *after* daemonizing or pid will be wrong
6827
- self._context.write_pidfile()
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
- self._event_callbacks.notify(SupervisorRunningEvent())
7415
+ return Dispatchers(dispatchers)
6830
7416
 
6831
- while True:
6832
- if callback is not None and not callback(self):
6833
- break
7417
+ #
6834
7418
 
6835
- self._run_once()
7419
+ def _spawn_as_parent(self, sp: SpawnedProcess) -> None:
7420
+ close_child_pipes(sp.pipes)
6836
7421
 
6837
- finally:
6838
- self._context.cleanup()
7422
+ self._pid_history[sp.pid] = self.process
6839
7423
 
6840
7424
  #
6841
7425
 
6842
- def _run_once(self) -> None:
6843
- self._poll()
6844
- self._reap()
6845
- self._signal_handler.handle_signals()
6846
- self._tick()
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
- if self._context.state < SupervisorState.RUNNING:
6849
- self._ordered_stop_groups_phase_2()
7439
+ #
6850
7440
 
6851
- def _ordered_stop_groups_phase_1(self) -> None:
6852
- if self._stop_groups:
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
- def _ordered_stop_groups_phase_2(self) -> None:
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
- def _poll(self) -> None:
6868
- combined_map = {}
6869
- combined_map.update(self.get_process_map())
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
- pgroups = list(self._process_groups)
6872
- pgroups.sort()
7453
+ #
6873
7454
 
6874
- if self._context.state < SupervisorState.RUNNING:
6875
- if not self._stopping:
6876
- # first time, set the stopping flag, do a notification and set stop_groups
6877
- self._stopping = True
6878
- self._stop_groups = pgroups[:]
6879
- self._event_callbacks.notify(SupervisorStoppingEvent())
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
- self._ordered_stop_groups_phase_1()
7463
+ #
6882
7464
 
6883
- if not self.shutdown_report():
6884
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
6885
- raise ExitNow
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
- for fd, dispatcher in combined_map.items():
6888
- if dispatcher.readable():
6889
- self._poller.register_readable(fd)
6890
- if dispatcher.writable():
6891
- self._poller.register_writable(fd)
7475
+ #
6892
7476
 
6893
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
6894
- r, w = self._poller.poll(timeout)
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
- for fd in r:
6897
- if fd in combined_map:
6898
- try:
6899
- dispatcher = combined_map[fd]
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
- for fd in w:
6918
- if fd in combined_map:
6919
- try:
6920
- dispatcher = combined_map[fd]
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
- for group in pgroups:
6937
- group.transition()
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
- def _reap(self, *, once: bool = False, depth: int = 0) -> None:
6940
- if depth >= 100:
6941
- return
7496
+ raise RuntimeError('Unreachable')
6942
7497
 
6943
- pid, sts = self._context.waitpid()
6944
- if not pid:
6945
- return
7498
+ def _prepare_child_fds(self, pipes: ProcessPipes) -> None:
7499
+ os.dup2(check_not_none(pipes.child_stdin), 0)
6946
7500
 
6947
- process = self._context.pid_history.get(pid, None)
6948
- if process is None:
6949
- _, msg = decode_wait_status(check_not_none(sts))
6950
- log.info('reaped unknown pid %s (%s)', pid, msg)
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
- process.finish(check_not_none(sts))
6953
- del self._context.pid_history[pid]
7506
+ os.dup2(check_not_none(pipes.child_stderr), 2)
6954
7507
 
6955
- if not once:
6956
- # keep reaping until no more kids to reap, but don't recurse infinitely
6957
- self._reap(once=False, depth=depth + 1)
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 _tick(self, now: ta.Optional[float] = None) -> None:
6960
- """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
7513
+ def _set_uid(self) -> ta.Optional[str]:
7514
+ if self.config.uid is None:
7515
+ return None
6961
7516
 
6962
- if now is None:
6963
- # now won't be None in unit tests
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
- last_tick = self._ticks.get(period)
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
- # ../inject.py
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.bind(get_poller_impl(), key=Poller, singleton=True),
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(ProcessGroups, singleton=True),
7570
+ inj.bind(ProcessGroupManager, singleton=True),
7007
7571
  inj.bind(Supervisor, singleton=True),
7008
7572
 
7009
- inj.bind_factory(ProcessGroupFactory, ProcessGroupImpl),
7010
- inj.bind_factory(ProcessFactory, ProcessImpl),
7573
+ inj.bind(PidHistory()),
7011
7574
 
7012
- inj.bind_factory(OutputDispatcherFactory, OutputDispatcherImpl),
7013
- inj.bind_factory(InputDispatcherFactory, InputDispatcherImpl),
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