ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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 +1825 -1217
  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 +22 -338
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -110
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +45 -13
  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} +99 -317
  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 +54 -78
  27. ominfra/supervisor/types.py +122 -39
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev125.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,114 +5357,166 @@ class ServerContext(abc.ABC):
4898
5357
  raise NotImplementedError
4899
5358
 
4900
5359
 
4901
- # class Dispatcher(abc.ABC):
4902
- # pass
4903
- #
4904
- #
4905
- # class OutputDispatcher(Dispatcher, abc.ABC):
4906
- # pass
4907
- #
4908
- #
4909
- # class InputDispatcher(Dispatcher, abc.ABC):
4910
- # pass
5360
+ ##
4911
5361
 
4912
5362
 
4913
- @functools.total_ordering
4914
- class Process(abc.ABC):
5363
+ class Dispatcher(abc.ABC):
4915
5364
  @property
4916
5365
  @abc.abstractmethod
4917
- def pid(self) -> int:
5366
+ def process(self) -> 'Process':
4918
5367
  raise NotImplementedError
4919
5368
 
4920
5369
  @property
4921
5370
  @abc.abstractmethod
4922
- def config(self) -> ProcessConfig:
5371
+ def channel(self) -> str:
4923
5372
  raise NotImplementedError
4924
5373
 
4925
- def __lt__(self, other):
4926
- return self.config.priority < other.config.priority
4927
-
4928
- def __eq__(self, other):
4929
- return self.config.priority == other.config.priority
4930
-
4931
5374
  @property
4932
5375
  @abc.abstractmethod
4933
- def context(self) -> ServerContext:
5376
+ def fd(self) -> int:
4934
5377
  raise NotImplementedError
4935
5378
 
5379
+ @property
4936
5380
  @abc.abstractmethod
4937
- def finish(self, sts: int) -> None:
5381
+ def closed(self) -> bool:
4938
5382
  raise NotImplementedError
4939
5383
 
5384
+ #
5385
+
4940
5386
  @abc.abstractmethod
4941
- def remove_logs(self) -> None:
5387
+ def close(self) -> None:
4942
5388
  raise NotImplementedError
4943
5389
 
4944
5390
  @abc.abstractmethod
4945
- def reopen_logs(self) -> None:
5391
+ def handle_error(self) -> None:
4946
5392
  raise NotImplementedError
4947
5393
 
5394
+ #
5395
+
4948
5396
  @abc.abstractmethod
4949
- def stop(self) -> ta.Optional[str]:
5397
+ def readable(self) -> bool:
4950
5398
  raise NotImplementedError
4951
5399
 
4952
5400
  @abc.abstractmethod
4953
- def give_up(self) -> None:
5401
+ def writable(self) -> bool:
4954
5402
  raise NotImplementedError
4955
5403
 
5404
+ #
5405
+
5406
+ def handle_read_event(self) -> None:
5407
+ raise TypeError
5408
+
5409
+ def handle_write_event(self) -> None:
5410
+ raise TypeError
5411
+
5412
+
5413
+ class OutputDispatcher(Dispatcher, abc.ABC):
4956
5414
  @abc.abstractmethod
4957
- def transition(self) -> None:
5415
+ def remove_logs(self) -> None:
4958
5416
  raise NotImplementedError
4959
5417
 
4960
5418
  @abc.abstractmethod
4961
- def get_state(self) -> ProcessState:
5419
+ def reopen_logs(self) -> None:
4962
5420
  raise NotImplementedError
4963
5421
 
5422
+
5423
+ class InputDispatcher(Dispatcher, abc.ABC):
4964
5424
  @abc.abstractmethod
4965
- def create_auto_child_logs(self) -> None:
5425
+ def write(self, chars: ta.Union[bytes, str]) -> None:
4966
5426
  raise NotImplementedError
4967
5427
 
4968
5428
  @abc.abstractmethod
4969
- def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
5429
+ def flush(self) -> None:
4970
5430
  raise NotImplementedError
4971
5431
 
4972
5432
 
4973
- @functools.total_ordering
4974
- class ProcessGroup(abc.ABC):
5433
+ ##
5434
+
5435
+
5436
+ class Process(ConfigPriorityOrdered, abc.ABC):
4975
5437
  @property
4976
5438
  @abc.abstractmethod
4977
- def config(self) -> ProcessGroupConfig:
5439
+ def name(self) -> str:
4978
5440
  raise NotImplementedError
4979
5441
 
4980
- def __lt__(self, other):
4981
- return self.config.priority < other.config.priority
5442
+ @property
5443
+ @abc.abstractmethod
5444
+ def config(self) -> ProcessConfig:
5445
+ raise NotImplementedError
4982
5446
 
4983
- def __eq__(self, other):
4984
- return self.config.priority == other.config.priority
5447
+ @property
5448
+ @abc.abstractmethod
5449
+ def group(self) -> 'ProcessGroup':
5450
+ raise NotImplementedError
5451
+
5452
+ @property
5453
+ @abc.abstractmethod
5454
+ def pid(self) -> int:
5455
+ raise NotImplementedError
5456
+
5457
+ #
5458
+
5459
+ @property
5460
+ @abc.abstractmethod
5461
+ def context(self) -> ServerContext:
5462
+ raise NotImplementedError
5463
+
5464
+ @abc.abstractmethod
5465
+ def finish(self, sts: int) -> None:
5466
+ raise NotImplementedError
5467
+
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
4985
5475
 
4986
5476
  @abc.abstractmethod
4987
5477
  def transition(self) -> None:
4988
5478
  raise NotImplementedError
4989
5479
 
4990
5480
  @abc.abstractmethod
4991
- def stop_all(self) -> None:
5481
+ def get_state(self) -> ProcessState:
5482
+ raise NotImplementedError
5483
+
5484
+ @abc.abstractmethod
5485
+ def after_setuid(self) -> None:
5486
+ raise NotImplementedError
5487
+
5488
+ @abc.abstractmethod
5489
+ def get_dispatchers(self) -> 'Dispatchers':
4992
5490
  raise NotImplementedError
4993
5491
 
5492
+
5493
+ ##
5494
+
5495
+
5496
+ class ProcessGroup(
5497
+ ConfigPriorityOrdered,
5498
+ KeyedCollectionAccessors[str, Process],
5499
+ abc.ABC,
5500
+ ):
4994
5501
  @property
4995
5502
  @abc.abstractmethod
4996
5503
  def name(self) -> str:
4997
5504
  raise NotImplementedError
4998
5505
 
5506
+ @property
4999
5507
  @abc.abstractmethod
5000
- def before_remove(self) -> None:
5508
+ def config(self) -> ProcessGroupConfig:
5001
5509
  raise NotImplementedError
5002
5510
 
5511
+ @property
5003
5512
  @abc.abstractmethod
5004
- def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
5513
+ def by_name(self) -> ta.Mapping[str, Process]:
5005
5514
  raise NotImplementedError
5006
5515
 
5516
+ #
5517
+
5007
5518
  @abc.abstractmethod
5008
- def reopen_logs(self) -> None:
5519
+ def stop_all(self) -> None:
5009
5520
  raise NotImplementedError
5010
5521
 
5011
5522
  @abc.abstractmethod
@@ -5013,7 +5524,7 @@ class ProcessGroup(abc.ABC):
5013
5524
  raise NotImplementedError
5014
5525
 
5015
5526
  @abc.abstractmethod
5016
- def after_setuid(self) -> None:
5527
+ def before_remove(self) -> None:
5017
5528
  raise NotImplementedError
5018
5529
 
5019
5530
 
@@ -5021,9 +5532,6 @@ class ProcessGroup(abc.ABC):
5021
5532
  # ../context.py
5022
5533
 
5023
5534
 
5024
- ServerEpoch = ta.NewType('ServerEpoch', int)
5025
-
5026
-
5027
5535
  class ServerContextImpl(ServerContext):
5028
5536
  def __init__(
5029
5537
  self,
@@ -5041,16 +5549,6 @@ class ServerContextImpl(ServerContext):
5041
5549
  self._pid_history: ta.Dict[int, Process] = {}
5042
5550
  self._state: SupervisorState = SupervisorState.RUNNING
5043
5551
 
5044
- if config.user is not None:
5045
- uid = name_to_uid(config.user)
5046
- self._uid: ta.Optional[int] = uid
5047
- self._gid: ta.Optional[int] = gid_for_uid(uid)
5048
- else:
5049
- self._uid = None
5050
- self._gid = None
5051
-
5052
- self._unlink_pidfile = False
5053
-
5054
5552
  @property
5055
5553
  def config(self) -> ServerConfig:
5056
5554
  return self._config
@@ -5074,15 +5572,7 @@ class ServerContextImpl(ServerContext):
5074
5572
  def pid_history(self) -> ta.Dict[int, Process]:
5075
5573
  return self._pid_history
5076
5574
 
5077
- @property
5078
- def uid(self) -> ta.Optional[int]:
5079
- return self._uid
5080
-
5081
- @property
5082
- def gid(self) -> ta.Optional[int]:
5083
- return self._gid
5084
-
5085
- ##
5575
+ #
5086
5576
 
5087
5577
  def waitpid(self) -> ta.Tuple[ta.Optional[int], ta.Optional[int]]:
5088
5578
  # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
@@ -5102,386 +5592,108 @@ class ServerContextImpl(ServerContext):
5102
5592
  pid, sts = None, None
5103
5593
  return pid, sts
5104
5594
 
5105
- def set_uid_or_exit(self) -> None:
5106
- """
5107
- Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
5108
- process via usage() if privileges could not be dropped.
5109
- """
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
5110
5603
 
5111
- if self.uid is None:
5112
- if os.getuid() == 0:
5113
- warnings.warn(
5114
- 'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
5115
- 'config file. If you intend to run as root, you can set user=root in the config file to avoid '
5116
- 'this message.',
5117
- )
5118
- else:
5119
- msg = drop_privileges(self.uid)
5120
- if msg is None:
5121
- log.info('Set uid to user %s succeeded', self.uid)
5122
- else: # failed to drop privileges
5123
- raise RuntimeError(msg)
5124
5604
 
5125
- def set_rlimits_or_exit(self) -> None:
5126
- """
5127
- Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
5128
- the process via usage() if any rlimits could not be set.
5129
- """
5605
+ ########################################
5606
+ # ../dispatchers.py
5130
5607
 
5131
- limits = []
5132
5608
 
5133
- if hasattr(resource, 'RLIMIT_NOFILE'):
5134
- limits.append({
5135
- 'msg': (
5136
- 'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
5137
- '"minfds" command-line argument or config file setting. The current environment will only allow '
5138
- 'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
5139
- 'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
5140
- 'process to start.'
5141
- ),
5142
- 'min': self.config.minfds,
5143
- 'resource': resource.RLIMIT_NOFILE,
5144
- 'name': 'RLIMIT_NOFILE',
5145
- })
5609
+ class Dispatchers(KeyedCollection[int, Dispatcher]):
5610
+ def _key(self, v: Dispatcher) -> int:
5611
+ return v.fd
5146
5612
 
5147
- if hasattr(resource, 'RLIMIT_NPROC'):
5148
- limits.append({
5149
- 'msg': (
5150
- 'The minimum number of available processes required to run this program is %(min_limit)s as per '
5151
- 'the "minprocs" command-line argument or config file setting. The current environment will only '
5152
- 'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
5153
- 'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
5154
- 'program to start.'
5155
- ),
5156
- 'min': self.config.minprocs,
5157
- 'resource': resource.RLIMIT_NPROC,
5158
- 'name': 'RLIMIT_NPROC',
5159
- })
5613
+ #
5160
5614
 
5161
- for limit in limits:
5162
- min_limit = limit['min']
5163
- res = limit['resource']
5164
- msg = limit['msg']
5165
- 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()
5166
5623
 
5167
- soft, hard = resource.getrlimit(res) # type: ignore
5624
+ #
5168
5625
 
5169
- # -1 means unlimited
5170
- if soft < min_limit and soft != -1: # type: ignore
5171
- if hard < min_limit and hard != -1: # type: ignore
5172
- # setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
5173
- # usage
5174
- 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()
5175
5630
 
5176
- try:
5177
- resource.setrlimit(res, (min_limit, hard)) # type: ignore
5178
- log.info('Increased %s limit to %s', name, min_limit)
5179
- except (resource.error, ValueError):
5180
- raise RuntimeError(msg % dict( # type: ignore # noqa
5181
- min_limit=min_limit,
5182
- res=res,
5183
- name=name,
5184
- soft=soft,
5185
- hard=hard,
5186
- ))
5631
+ def reopen_logs(self) -> None:
5632
+ for d in self:
5633
+ if isinstance(d, OutputDispatcher):
5634
+ d.reopen_logs()
5187
5635
 
5188
- def cleanup(self) -> None:
5189
- if self._unlink_pidfile:
5190
- try_unlink(self.config.pidfile)
5191
- self._poller.close()
5192
5636
 
5193
- def cleanup_fds(self) -> None:
5194
- # try to close any leaked file descriptors (for reload)
5195
- start = 5
5196
- os.closerange(start, self.config.minfds)
5637
+ ########################################
5638
+ # ../dispatchersimpl.py
5197
5639
 
5198
- def clear_auto_child_logdir(self) -> None:
5199
- # must be called after realize()
5200
- child_logdir = self.config.child_logdir
5201
- fnre = re.compile(rf'.+?---{self.config.identifier}-\S+\.log\.?\d{{0,4}}')
5202
- try:
5203
- filenames = os.listdir(child_logdir)
5204
- except OSError:
5205
- log.warning('Could not clear child_log dir')
5206
- return
5207
5640
 
5208
- for filename in filenames:
5209
- if fnre.match(filename):
5210
- pathname = os.path.join(child_logdir, filename)
5211
- try:
5212
- os.remove(pathname)
5213
- except OSError:
5214
- 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__()
5215
5651
 
5216
- def daemonize(self) -> None:
5217
- self._poller.before_daemonize()
5218
- self._daemonize()
5219
- 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
5220
5656
 
5221
- def _daemonize(self) -> None:
5222
- # To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
5223
- # our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
5224
- # our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
5225
- # terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
5226
- # use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
5227
- # session and process group and setting itself up as a new session leader.
5228
- #
5229
- # Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
5230
- # of ourselves that is guaranteed to not be a session group leader.
5231
- #
5232
- # We also change directories, set stderr and stdout to null, and change our umask.
5233
- #
5234
- # This explanation was (gratefully) garnered from
5235
- # http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
5657
+ self._closed = False # True if close() has been called
5236
5658
 
5237
- pid = os.fork()
5238
- if pid != 0:
5239
- # Parent
5240
- log.debug('supervisord forked; parent exiting')
5241
- real_exit(0)
5659
+ #
5242
5660
 
5243
- # Child
5244
- log.info('daemonizing the supervisord process')
5245
- if self.config.directory:
5246
- try:
5247
- os.chdir(self.config.directory)
5248
- except OSError as err:
5249
- log.critical("can't chdir into %r: %s", self.config.directory, err)
5250
- else:
5251
- 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})>'
5252
5663
 
5253
- os.dup2(0, os.open('/dev/null', os.O_RDONLY))
5254
- os.dup2(1, os.open('/dev/null', os.O_WRONLY))
5255
- os.dup2(2, os.open('/dev/null', os.O_WRONLY))
5664
+ #
5256
5665
 
5257
- os.setsid()
5666
+ @property
5667
+ def process(self) -> Process:
5668
+ return self._process
5258
5669
 
5259
- os.umask(self.config.umask)
5670
+ @property
5671
+ def channel(self) -> str:
5672
+ return self._channel
5260
5673
 
5261
- # XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
5262
- # file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
5263
- # again after the setsid() call, for obscure SVR4 reasons.
5674
+ @property
5675
+ def fd(self) -> int:
5676
+ return self._fd
5264
5677
 
5265
- def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
5266
- prefix = f'{name}-{channel}---{identifier}-'
5267
- logfile = mktempfile(
5268
- suffix='.log',
5269
- prefix=prefix,
5270
- dir=self.config.child_logdir,
5271
- )
5272
- return logfile
5678
+ @property
5679
+ def closed(self) -> bool:
5680
+ return self._closed
5273
5681
 
5274
- def write_pidfile(self) -> None:
5275
- pid = os.getpid()
5276
- try:
5277
- with open(self.config.pidfile, 'w') as f:
5278
- f.write(f'{pid}\n')
5279
- except OSError:
5280
- log.critical('could not write pidfile %s', self.config.pidfile)
5281
- else:
5282
- self._unlink_pidfile = True
5283
- log.info('supervisord started with pid %s', pid)
5682
+ #
5284
5683
 
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
5285
5688
 
5286
- def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
5287
- """
5288
- Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
5289
- and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
5290
- dropped.
5291
- """
5292
-
5293
- if user is None:
5294
- return 'No user specified to setuid to!'
5295
-
5296
- # get uid for user, which can be a number or username
5297
- try:
5298
- uid = int(user)
5299
- except ValueError:
5300
- try:
5301
- pwrec = pwd.getpwnam(user) # type: ignore
5302
- except KeyError:
5303
- return f"Can't find username {user!r}"
5304
- uid = pwrec[2]
5305
- else:
5306
- try:
5307
- pwrec = pwd.getpwuid(uid)
5308
- except KeyError:
5309
- return f"Can't find uid {uid!r}"
5310
-
5311
- current_uid = os.getuid()
5312
-
5313
- if current_uid == uid:
5314
- # do nothing and return successfully if the uid is already the current one. this allows a supervisord
5315
- # running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
5316
- # it.
5317
- return None
5318
-
5319
- if current_uid != 0:
5320
- return "Can't drop privilege as nonroot user"
5321
-
5322
- gid = pwrec[3]
5323
- if hasattr(os, 'setgroups'):
5324
- user = pwrec[0]
5325
- groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
5326
-
5327
- # always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
5328
- # group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
5329
- # python 2.7 - this will be safe though for all unix /python version combos)
5330
- groups.insert(0, gid)
5331
- try:
5332
- os.setgroups(groups)
5333
- except OSError:
5334
- return 'Could not set groups of effective user'
5335
-
5336
- try:
5337
- os.setgid(gid)
5338
- except OSError:
5339
- return 'Could not set group id of effective user'
5340
-
5341
- os.setuid(uid)
5342
-
5343
- return None
5344
-
5345
-
5346
- def make_pipes(stderr=True) -> ta.Mapping[str, int]:
5347
- """
5348
- Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
5349
- read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
5350
- """
5351
-
5352
- pipes: ta.Dict[str, ta.Optional[int]] = {
5353
- 'child_stdin': None,
5354
- 'stdin': None,
5355
- 'stdout': None,
5356
- 'child_stdout': None,
5357
- 'stderr': None,
5358
- 'child_stderr': None,
5359
- }
5360
-
5361
- try:
5362
- stdin, child_stdin = os.pipe()
5363
- pipes['child_stdin'], pipes['stdin'] = stdin, child_stdin
5364
-
5365
- stdout, child_stdout = os.pipe()
5366
- pipes['stdout'], pipes['child_stdout'] = stdout, child_stdout
5367
-
5368
- if stderr:
5369
- stderr, child_stderr = os.pipe()
5370
- pipes['stderr'], pipes['child_stderr'] = stderr, child_stderr
5371
-
5372
- for fd in (pipes['stdout'], pipes['stderr'], pipes['stdin']):
5373
- if fd is not None:
5374
- flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
5375
- fcntl.fcntl(fd, fcntl.F_SETFL, flags)
5376
-
5377
- return pipes # type: ignore
5378
-
5379
- except OSError:
5380
- for fd in pipes.values():
5381
- if fd is not None:
5382
- close_fd(fd)
5383
- raise
5384
-
5385
-
5386
- def close_parent_pipes(pipes: ta.Mapping[str, int]) -> None:
5387
- for fdname in ('stdin', 'stdout', 'stderr'):
5388
- fd = pipes.get(fdname)
5389
- if fd is not None:
5390
- close_fd(fd)
5391
-
5392
-
5393
- def close_child_pipes(pipes: ta.Mapping[str, int]) -> None:
5394
- for fdname in ('child_stdin', 'child_stdout', 'child_stderr'):
5395
- fd = pipes.get(fdname)
5396
- if fd is not None:
5397
- close_fd(fd)
5398
-
5399
-
5400
- def check_execv_args(filename, argv, st) -> None:
5401
- if st is None:
5402
- raise NotFoundError(f"can't find command {filename!r}")
5403
-
5404
- elif stat.S_ISDIR(st[stat.ST_MODE]):
5405
- raise NotExecutableError(f'command at {filename!r} is a directory')
5406
-
5407
- elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
5408
- raise NotExecutableError(f'command at {filename!r} is not executable')
5409
-
5410
- elif not os.access(filename, os.X_OK):
5411
- raise NoPermissionError(f'no permission to run command {filename!r}')
5412
-
5413
-
5414
- ########################################
5415
- # ../dispatchers.py
5416
-
5417
-
5418
- class Dispatcher(abc.ABC):
5419
- def __init__(
5420
- self,
5421
- process: Process,
5422
- channel: str,
5423
- fd: int,
5424
- *,
5425
- event_callbacks: EventCallbacks,
5426
- ) -> None:
5427
- super().__init__()
5428
-
5429
- self._process = process # process which "owns" this dispatcher
5430
- self._channel = channel # 'stderr' or 'stdout'
5431
- self._fd = fd
5432
- self._event_callbacks = event_callbacks
5433
-
5434
- self._closed = False # True if close() has been called
5435
-
5436
- def __repr__(self) -> str:
5437
- return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
5438
-
5439
- @property
5440
- def process(self) -> Process:
5441
- return self._process
5442
-
5443
- @property
5444
- def channel(self) -> str:
5445
- return self._channel
5446
-
5447
- @property
5448
- def fd(self) -> int:
5449
- return self._fd
5450
-
5451
- @property
5452
- def closed(self) -> bool:
5453
- return self._closed
5454
-
5455
- @abc.abstractmethod
5456
- def readable(self) -> bool:
5457
- raise NotImplementedError
5458
-
5459
- @abc.abstractmethod
5460
- def writable(self) -> bool:
5461
- raise NotImplementedError
5462
-
5463
- def handle_read_event(self) -> None:
5464
- raise TypeError
5465
-
5466
- def handle_write_event(self) -> None:
5467
- raise TypeError
5468
-
5469
- def handle_error(self) -> None:
5470
- nil, t, v, tbinfo = compact_traceback()
5689
+ def handle_error(self) -> None:
5690
+ nil, t, v, tbinfo = compact_traceback()
5471
5691
 
5472
5692
  log.critical('uncaptured python exception, closing channel %s (%s:%s %s)', repr(self), t, v, tbinfo)
5473
5693
  self.close()
5474
5694
 
5475
- def close(self) -> None:
5476
- if not self._closed:
5477
- log.debug('fd %s closed, stopped monitoring %s', self._fd, self)
5478
- self._closed = True
5479
5695
 
5480
- def flush(self) -> None: # noqa
5481
- pass
5482
-
5483
-
5484
- class OutputDispatcher(Dispatcher):
5696
+ class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5485
5697
  """
5486
5698
  Dispatcher for one channel (stdout or stderr) of one process. Serves several purposes:
5487
5699
 
@@ -5495,13 +5707,14 @@ class OutputDispatcher(Dispatcher):
5495
5707
  process: Process,
5496
5708
  event_type: ta.Type[ProcessCommunicationEvent],
5497
5709
  fd: int,
5498
- **kwargs: ta.Any,
5710
+ *,
5711
+ event_callbacks: EventCallbacks,
5499
5712
  ) -> None:
5500
5713
  super().__init__(
5501
5714
  process,
5502
5715
  event_type.channel,
5503
5716
  fd,
5504
- **kwargs,
5717
+ event_callbacks=event_callbacks,
5505
5718
  )
5506
5719
 
5507
5720
  self._event_type = event_type
@@ -5698,19 +5911,20 @@ class OutputDispatcher(Dispatcher):
5698
5911
  self.close()
5699
5912
 
5700
5913
 
5701
- class InputDispatcher(Dispatcher):
5914
+ class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
5702
5915
  def __init__(
5703
5916
  self,
5704
5917
  process: Process,
5705
5918
  channel: str,
5706
5919
  fd: int,
5707
- **kwargs: ta.Any,
5920
+ *,
5921
+ event_callbacks: EventCallbacks,
5708
5922
  ) -> None:
5709
5923
  super().__init__(
5710
5924
  process,
5711
5925
  channel,
5712
5926
  fd,
5713
- **kwargs,
5927
+ event_callbacks=event_callbacks,
5714
5928
  )
5715
5929
 
5716
5930
  self._input_buffer = b''
@@ -5747,58 +5961,133 @@ class InputDispatcher(Dispatcher):
5747
5961
  # ../groups.py
5748
5962
 
5749
5963
 
5750
- ##
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
5751
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
+ #
5752
6010
 
5753
- ProcessFactory = ta.NewType('ProcessFactory', Func[Process]) # (config: ProcessConfig, group: ProcessGroup)
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]
6018
+
6019
+ cur_by_name = {cfg.name: cfg for cfg in cur}
6020
+ new_by_name = {cfg.name: cfg for cfg in new}
6021
+
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
5754
6039
 
5755
6040
 
5756
6041
  class ProcessGroupImpl(ProcessGroup):
5757
6042
  def __init__(
5758
6043
  self,
5759
6044
  config: ProcessGroupConfig,
5760
- context: ServerContext,
5761
6045
  *,
5762
6046
  process_factory: ProcessFactory,
5763
6047
  ):
5764
6048
  super().__init__()
5765
6049
 
5766
6050
  self._config = config
5767
- self._context = context
5768
6051
  self._process_factory = process_factory
5769
6052
 
5770
- self._processes = {}
6053
+ by_name: ta.Dict[str, Process] = {}
5771
6054
  for pconfig in self._config.processes or []:
5772
- process = self._process_factory(pconfig, self)
5773
- 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
5774
6060
 
5775
6061
  @property
5776
- def config(self) -> ProcessGroupConfig:
5777
- 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
+ #
5778
6071
 
5779
6072
  @property
5780
6073
  def name(self) -> str:
5781
6074
  return self._config.name
5782
6075
 
5783
6076
  @property
5784
- def context(self) -> ServerContext:
5785
- return self._context
6077
+ def config(self) -> ProcessGroupConfig:
6078
+ return self._config
5786
6079
 
5787
- def __repr__(self):
5788
- # repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
5789
- name = self._config.name
5790
- 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
5791
6083
 
5792
- def remove_logs(self) -> None:
5793
- for process in self._processes.values():
5794
- process.remove_logs()
6084
+ #
5795
6085
 
5796
- def reopen_logs(self) -> None:
5797
- for process in self._processes.values():
5798
- 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]
5799
6088
 
5800
6089
  def stop_all(self) -> None:
5801
- processes = list(self._processes.values())
6090
+ processes = list(self._by_name.values())
5802
6091
  processes.sort()
5803
6092
  processes.reverse() # stop in desc priority order
5804
6093
 
@@ -5816,84 +6105,642 @@ class ProcessGroupImpl(ProcessGroup):
5816
6105
  # BACKOFF -> FATAL
5817
6106
  proc.give_up()
5818
6107
 
5819
- def get_unstopped_processes(self) -> ta.List[Process]:
5820
- return [x for x in self._processes.values() if not x.get_state().stopped]
5821
-
5822
- def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
5823
- dispatchers: dict = {}
5824
- for process in self._processes.values():
5825
- dispatchers.update(process.get_dispatchers())
5826
- return dispatchers
5827
-
5828
6108
  def before_remove(self) -> None:
5829
6109
  pass
5830
6110
 
5831
- def transition(self) -> None:
5832
- for proc in self._processes.values():
5833
- proc.transition()
5834
6111
 
5835
- def after_setuid(self) -> None:
5836
- for proc in self._processes.values():
5837
- proc.create_auto_child_logs()
6112
+ ########################################
6113
+ # ../processes.py
5838
6114
 
5839
6115
 
5840
6116
  ##
5841
6117
 
5842
6118
 
5843
- class ProcessGroups:
5844
- def __init__(
5845
- self,
5846
- *,
5847
- event_callbacks: EventCallbacks,
5848
- ) -> None:
5849
- super().__init__()
6119
+ class ProcessStateError(RuntimeError):
6120
+ pass
5850
6121
 
6122
+
6123
+ ##
6124
+
6125
+
6126
+ class PidHistory(ta.Dict[int, Process]):
6127
+ pass
6128
+
6129
+
6130
+ ########################################
6131
+ # ../setupimpl.py
6132
+
6133
+
6134
+ ##
6135
+
6136
+
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__()
6147
+
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
5851
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)
5852
6523
 
5853
- self._by_name: ta.Dict[str, ProcessGroup] = {}
6524
+ return True
5854
6525
 
5855
- def get(self, name: str) -> ta.Optional[ProcessGroup]:
5856
- return self._by_name.get(name)
6526
+ def remove_process_group(self, name: str) -> bool:
6527
+ if self._process_groups[name].get_unstopped_processes():
6528
+ return False
5857
6529
 
5858
- def __getitem__(self, name: str) -> ProcessGroup:
5859
- return self._by_name[name]
6530
+ self._process_groups.remove(name)
5860
6531
 
5861
- def __len__(self) -> int:
5862
- return len(self._by_name)
6532
+ return True
5863
6533
 
5864
- def __iter__(self) -> ta.Iterator[ProcessGroup]:
5865
- return iter(self._by_name.values())
6534
+ #
5866
6535
 
5867
- def all(self) -> ta.Mapping[str, ProcessGroup]:
5868
- return self._by_name
6536
+ def shutdown_report(self) -> ta.List[Process]:
6537
+ unstopped: ta.List[Process] = []
5869
6538
 
5870
- def add(self, group: ProcessGroup) -> None:
5871
- if (name := group.name) in self._by_name:
5872
- raise KeyError(f'Process group already exists: {name}')
6539
+ for group in self._process_groups:
6540
+ unstopped.extend(group.get_unstopped_processes())
5873
6541
 
5874
- self._by_name[name] = group
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)
5875
6552
 
5876
- self._event_callbacks.notify(ProcessGroupAddedEvent(name))
6553
+ return unstopped
5877
6554
 
5878
- def remove(self, name: str) -> None:
5879
- group = self._by_name[name]
6555
+ #
5880
6556
 
5881
- group.before_remove()
6557
+ def main(self, **kwargs: ta.Any) -> None:
6558
+ self._setup.setup()
6559
+ try:
6560
+ self.run(**kwargs)
6561
+ finally:
6562
+ self._setup.cleanup()
5882
6563
 
5883
- del self._by_name[name]
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
5884
6571
 
5885
- self._event_callbacks.notify(ProcessGroupRemovedEvent(name))
6572
+ self._event_callbacks.clear()
5886
6573
 
5887
- def clear(self) -> None:
5888
- # FIXME: events?
5889
- self._by_name.clear()
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))
5890
6736
 
5891
6737
 
5892
6738
  ########################################
5893
- # ../process.py
6739
+ # ../processesimpl.py
5894
6740
 
5895
6741
 
5896
- InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
6742
+ class ProcessSpawningFactory(Func1[Process, ProcessSpawning]):
6743
+ pass
5897
6744
 
5898
6745
 
5899
6746
  ##
@@ -5909,19 +6756,22 @@ class ProcessImpl(Process):
5909
6756
  *,
5910
6757
  context: ServerContext,
5911
6758
  event_callbacks: EventCallbacks,
5912
-
5913
- inherited_fds: ta.Optional[InheritedFds] = None,
6759
+ process_spawning_factory: ProcessSpawningFactory,
5914
6760
  ) -> None:
5915
6761
  super().__init__()
5916
6762
 
5917
6763
  self._config = config
5918
6764
  self._group = group
6765
+
5919
6766
  self._context = context
5920
6767
  self._event_callbacks = event_callbacks
5921
- self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
5922
6768
 
5923
- self._dispatchers: ta.Dict[int, Dispatcher] = {}
5924
- self._pipes: ta.Dict[str, int] = {}
6769
+ self._spawning = process_spawning_factory(self)
6770
+
6771
+ #
6772
+
6773
+ self._dispatchers = Dispatchers([])
6774
+ self._pipes = ProcessPipes()
5925
6775
 
5926
6776
  self._state = ProcessState.STOPPED
5927
6777
  self._pid = 0 # 0 when not running
@@ -5938,144 +6788,47 @@ class ProcessImpl(Process):
5938
6788
 
5939
6789
  self._backoff = 0 # backoff counter (to startretries)
5940
6790
 
5941
- self._exitstatus: ta.Optional[int] = None # status attached to dead process by finish()
5942
- self._spawn_err: ta.Optional[str] = None # error message attached by spawn() if any
5943
-
5944
- @property
5945
- def pid(self) -> int:
5946
- return self._pid
5947
-
5948
- @property
5949
- def group(self) -> ProcessGroup:
5950
- return self._group
5951
-
5952
- @property
5953
- def config(self) -> ProcessConfig:
5954
- return self._config
5955
-
5956
- @property
5957
- def context(self) -> ServerContext:
5958
- return self._context
5959
-
5960
- @property
5961
- def state(self) -> ProcessState:
5962
- return self._state
5963
-
5964
- @property
5965
- def backoff(self) -> int:
5966
- return self._backoff
5967
-
5968
- def get_dispatchers(self) -> ta.Mapping[int, Dispatcher]:
5969
- return self._dispatchers
5970
-
5971
- def remove_logs(self) -> None:
5972
- for dispatcher in self._dispatchers.values():
5973
- if hasattr(dispatcher, 'remove_logs'):
5974
- dispatcher.remove_logs()
5975
-
5976
- def reopen_logs(self) -> None:
5977
- for dispatcher in self._dispatchers.values():
5978
- if hasattr(dispatcher, 'reopen_logs'):
5979
- dispatcher.reopen_logs()
5980
-
5981
- def drain(self) -> None:
5982
- for dispatcher in self._dispatchers.values():
5983
- # note that we *must* call readable() for every dispatcher, as it may have side effects for a given
5984
- # dispatcher (eg. call handle_listener_state_change for event listener processes)
5985
- if dispatcher.readable():
5986
- dispatcher.handle_read_event()
5987
- if dispatcher.writable():
5988
- dispatcher.handle_write_event()
5989
-
5990
- def write(self, chars: ta.Union[bytes, str]) -> None:
5991
- if not self.pid or self._killing:
5992
- raise OSError(errno.EPIPE, 'Process already closed')
5993
-
5994
- stdin_fd = self._pipes['stdin']
5995
- if stdin_fd is None:
5996
- raise OSError(errno.EPIPE, 'Process has no stdin channel')
5997
-
5998
- dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
5999
- if dispatcher.closed:
6000
- raise OSError(errno.EPIPE, "Process' stdin channel is closed")
6001
-
6002
- dispatcher.write(chars)
6003
- dispatcher.flush() # this must raise EPIPE if the pipe is closed
6004
-
6005
- def _get_execv_args(self) -> ta.Tuple[str, ta.Sequence[str]]:
6006
- """
6007
- Internal: turn a program name into a file name, using $PATH, make sure it exists / is executable, raising a
6008
- ProcessError if not
6009
- """
6010
-
6011
- try:
6012
- commandargs = shlex.split(self._config.command)
6013
- except ValueError as e:
6014
- raise BadCommandError(f"can't parse command {self._config.command!r}: {e}") # noqa
6791
+ self._exitstatus: ta.Optional[int] = None # status attached to dead process by finish()
6792
+ self._spawn_err: ta.Optional[str] = None # error message attached by spawn() if any
6015
6793
 
6016
- if commandargs:
6017
- program = commandargs[0]
6018
- else:
6019
- raise BadCommandError('command is empty')
6794
+ #
6020
6795
 
6021
- if '/' in program:
6022
- filename = program
6023
- try:
6024
- st = os.stat(filename)
6025
- except OSError:
6026
- st = None
6796
+ def __repr__(self) -> str:
6797
+ return f'<Subprocess at {id(self)} with name {self._config.name} in state {self.get_state().name}>'
6027
6798
 
6028
- else:
6029
- path = get_path()
6030
- found = None
6031
- st = None
6032
- for dir in path: # noqa
6033
- found = os.path.join(dir, program)
6034
- try:
6035
- st = os.stat(found)
6036
- except OSError:
6037
- pass
6038
- else:
6039
- break
6040
- if st is None:
6041
- filename = program
6042
- else:
6043
- filename = found # type: ignore
6799
+ #
6044
6800
 
6045
- # check_execv_args will raise a ProcessError if the execv args are bogus, we break it out into a separate
6046
- # options method call here only to service unit tests
6047
- check_execv_args(filename, commandargs, st)
6801
+ @property
6802
+ def name(self) -> str:
6803
+ return self._config.name
6804
+
6805
+ @property
6806
+ def config(self) -> ProcessConfig:
6807
+ return self._config
6048
6808
 
6049
- return filename, commandargs
6809
+ @property
6810
+ def group(self) -> ProcessGroup:
6811
+ return self._group
6050
6812
 
6051
- def change_state(self, new_state: ProcessState, expected: bool = True) -> bool:
6052
- old_state = self._state
6053
- if new_state is old_state:
6054
- return False
6813
+ @property
6814
+ def pid(self) -> int:
6815
+ return self._pid
6055
6816
 
6056
- self._state = new_state
6057
- if new_state == ProcessState.BACKOFF:
6058
- now = time.time()
6059
- self._backoff += 1
6060
- self._delay = now + self._backoff
6817
+ #
6061
6818
 
6062
- event_class = PROCESS_STATE_EVENT_MAP.get(new_state)
6063
- if event_class is not None:
6064
- event = event_class(self, old_state, expected)
6065
- self._event_callbacks.notify(event)
6819
+ @property
6820
+ def context(self) -> ServerContext:
6821
+ return self._context
6066
6822
 
6067
- return True
6823
+ @property
6824
+ def state(self) -> ProcessState:
6825
+ return self._state
6068
6826
 
6069
- def _check_in_state(self, *states: ProcessState) -> None:
6070
- if self._state not in states:
6071
- current_state = self._state.name
6072
- allowable_states = ' '.join(s.name for s in states)
6073
- process_name = as_string(self._config.name)
6074
- 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
6075
6830
 
6076
- def _record_spawn_err(self, msg: str) -> None:
6077
- self._spawn_err = msg
6078
- log.info('_spawn_err: %s', msg)
6831
+ #
6079
6832
 
6080
6833
  def spawn(self) -> ta.Optional[int]:
6081
6834
  process_name = as_string(self._config.name)
@@ -6084,6 +6837,13 @@ class ProcessImpl(Process):
6084
6837
  log.warning('process \'%s\' already running', process_name)
6085
6838
  return None
6086
6839
 
6840
+ self.check_in_state(
6841
+ ProcessState.EXITED,
6842
+ ProcessState.FATAL,
6843
+ ProcessState.BACKOFF,
6844
+ ProcessState.STOPPED,
6845
+ )
6846
+
6087
6847
  self._killing = False
6088
6848
  self._spawn_err = None
6089
6849
  self._exitstatus = None
@@ -6092,183 +6852,73 @@ class ProcessImpl(Process):
6092
6852
 
6093
6853
  self._last_start = time.time()
6094
6854
 
6095
- self._check_in_state(
6096
- ProcessState.EXITED,
6097
- ProcessState.FATAL,
6098
- ProcessState.BACKOFF,
6099
- ProcessState.STOPPED,
6100
- )
6101
-
6102
6855
  self.change_state(ProcessState.STARTING)
6103
6856
 
6104
6857
  try:
6105
- filename, argv = self._get_execv_args()
6106
- except ProcessError as what:
6107
- self._record_spawn_err(what.args[0])
6108
- self._check_in_state(ProcessState.STARTING)
6109
- self.change_state(ProcessState.BACKOFF)
6110
- return None
6111
-
6112
- try:
6113
- self._dispatchers, self._pipes = self._make_dispatchers() # type: ignore
6114
- except OSError as why:
6115
- code = why.args[0]
6116
- if code == errno.EMFILE:
6117
- # too many file descriptors open
6118
- msg = f"too many open files to spawn '{process_name}'"
6119
- else:
6120
- msg = f"unknown error making dispatchers for '{process_name}': {errno.errorcode.get(code, code)}"
6121
- self._record_spawn_err(msg)
6122
- self._check_in_state(ProcessState.STARTING)
6123
- self.change_state(ProcessState.BACKOFF)
6124
- return None
6125
-
6126
- try:
6127
- pid = os.fork()
6128
- except OSError as why:
6129
- code = why.args[0]
6130
- if code == errno.EAGAIN:
6131
- # process table full
6132
- msg = f'Too many processes in process table to spawn \'{process_name}\''
6133
- else:
6134
- msg = f'unknown error during fork for \'{process_name}\': {errno.errorcode.get(code, code)}'
6135
- self._record_spawn_err(msg)
6136
- 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)
6137
6863
  self.change_state(ProcessState.BACKOFF)
6138
- close_parent_pipes(self._pipes)
6139
- close_child_pipes(self._pipes)
6140
- return None
6141
-
6142
- if pid != 0:
6143
- return self._spawn_as_parent(pid)
6144
-
6145
- else:
6146
- self._spawn_as_child(filename, argv)
6147
6864
  return None
6148
6865
 
6149
- def _make_dispatchers(self) -> ta.Tuple[ta.Mapping[int, Dispatcher], ta.Mapping[str, int]]:
6150
- use_stderr = not self._config.redirect_stderr
6151
-
6152
- p = make_pipes(use_stderr)
6153
- stdout_fd, stderr_fd, stdin_fd = p['stdout'], p['stderr'], p['stdin']
6866
+ log.info("Spawned: '%s' with pid %s", self.name, sp.pid)
6154
6867
 
6155
- dispatchers: ta.Dict[int, Dispatcher] = {}
6868
+ self._pid = sp.pid
6869
+ self._pipes = sp.pipes
6870
+ self._dispatchers = sp.dispatchers
6156
6871
 
6157
- dispatcher_kw = dict(
6158
- event_callbacks=self._event_callbacks,
6159
- )
6872
+ self._delay = time.time() + self.config.startsecs
6160
6873
 
6161
- etype: ta.Type[ProcessCommunicationEvent]
6162
- if stdout_fd is not None:
6163
- etype = ProcessCommunicationStdoutEvent
6164
- dispatchers[stdout_fd] = OutputDispatcher(
6165
- self,
6166
- etype,
6167
- stdout_fd,
6168
- **dispatcher_kw,
6169
- )
6874
+ return sp.pid
6170
6875
 
6171
- if stderr_fd is not None:
6172
- etype = ProcessCommunicationStderrEvent
6173
- dispatchers[stderr_fd] = OutputDispatcher(
6174
- self,
6175
- etype,
6176
- stderr_fd,
6177
- **dispatcher_kw,
6178
- )
6876
+ def get_dispatchers(self) -> Dispatchers:
6877
+ return self._dispatchers
6179
6878
 
6180
- if stdin_fd is not None:
6181
- dispatchers[stdin_fd] = InputDispatcher(
6182
- self,
6183
- 'stdin',
6184
- stdin_fd,
6185
- **dispatcher_kw,
6186
- )
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')
6187
6882
 
6188
- 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')
6189
6886
 
6190
- def _spawn_as_parent(self, pid: int) -> int:
6191
- # Parent
6192
- self._pid = pid
6193
- close_child_pipes(self._pipes)
6194
- log.info('spawned: \'%s\' with pid %s', as_string(self._config.name), pid)
6195
- self._spawn_err = None
6196
- self._delay = time.time() + self._config.startsecs
6197
- self.context.pid_history[pid] = self
6198
- return pid
6199
-
6200
- def _prepare_child_fds(self) -> None:
6201
- os.dup2(self._pipes['child_stdin'], 0)
6202
- os.dup2(self._pipes['child_stdout'], 1)
6203
- if self._config.redirect_stderr:
6204
- os.dup2(self._pipes['child_stdout'], 2)
6205
- else:
6206
- 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")
6207
6890
 
6208
- for i in range(3, self.context.config.minfds):
6209
- if i in self._inherited_fds:
6210
- continue
6211
- close_fd(i)
6891
+ dispatcher.write(chars)
6892
+ dispatcher.flush() # this must raise EPIPE if the pipe is closed
6212
6893
 
6213
- def _spawn_as_child(self, filename: str, argv: ta.Sequence[str]) -> None:
6214
- try:
6215
- # prevent child from receiving signals sent to the parent by calling os.setpgrp to create a new process
6216
- # group for the child; this prevents, for instance, the case of child processes being sent a SIGINT when
6217
- # running supervisor in foreground mode and Ctrl-C in the terminal window running supervisord is pressed.
6218
- # Presumably it also prevents HUP, etc received by supervisord from being sent to children.
6219
- os.setpgrp()
6894
+ #
6220
6895
 
6221
- self._prepare_child_fds()
6222
- # 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
6223
6900
 
6224
- # set user
6225
- setuid_msg = self.set_uid()
6226
- if setuid_msg:
6227
- uid = self._config.uid
6228
- msg = f"couldn't setuid to {uid}: {setuid_msg}\n"
6229
- os.write(2, as_bytes('supervisor: ' + msg))
6230
- 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
6231
6906
 
6232
- # set environment
6233
- env = os.environ.copy()
6234
- env['SUPERVISOR_ENABLED'] = '1'
6235
- env['SUPERVISOR_PROCESS_NAME'] = self._config.name
6236
- if self._group:
6237
- env['SUPERVISOR_GROUP_NAME'] = self._group.config.name
6238
- if self._config.environment is not None:
6239
- env.update(self._config.environment)
6240
-
6241
- # change directory
6242
- cwd = self._config.directory
6243
- try:
6244
- if cwd is not None:
6245
- os.chdir(os.path.expanduser(cwd))
6246
- except OSError as why:
6247
- code = errno.errorcode.get(why.args[0], why.args[0])
6248
- msg = f"couldn't chdir to {cwd}: {code}\n"
6249
- os.write(2, as_bytes('supervisor: ' + msg))
6250
- 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)
6251
6911
 
6252
- # set umask, then execve
6253
- try:
6254
- if self._config.umask is not None:
6255
- os.umask(self._config.umask)
6256
- os.execve(filename, list(argv), env)
6257
- except OSError as why:
6258
- code = errno.errorcode.get(why.args[0], why.args[0])
6259
- msg = f"couldn't exec {argv[0]}: {code}\n"
6260
- os.write(2, as_bytes('supervisor: ' + msg))
6261
- except Exception: # noqa
6262
- (file, fun, line), t, v, tbinfo = compact_traceback()
6263
- error = f'{t}, {v}: file: {file} line: {line}'
6264
- msg = f"couldn't exec {filename}: {error}\n"
6265
- os.write(2, as_bytes('supervisor: ' + msg))
6912
+ return True
6266
6913
 
6267
- # 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
+ )
6268
6920
 
6269
- finally:
6270
- os.write(2, as_bytes('supervisor: child process was not spawned\n'))
6271
- real_exit(127) # exit process with code for spawn failure
6921
+ #
6272
6922
 
6273
6923
  def _check_and_adjust_for_system_clock_rollback(self, test_time):
6274
6924
  """
@@ -6314,7 +6964,7 @@ class ProcessImpl(Process):
6314
6964
  self._delay = 0
6315
6965
  self._backoff = 0
6316
6966
  self._system_stop = True
6317
- self._check_in_state(ProcessState.BACKOFF)
6967
+ self.check_in_state(ProcessState.BACKOFF)
6318
6968
  self.change_state(ProcessState.FATAL)
6319
6969
 
6320
6970
  def kill(self, sig: int) -> ta.Optional[str]:
@@ -6358,7 +7008,7 @@ class ProcessImpl(Process):
6358
7008
  self._killing = True
6359
7009
  self._delay = now + self._config.stopwaitsecs
6360
7010
  # we will already be in the STOPPING state if we're doing a SIGKILL as a result of overrunning stopwaitsecs
6361
- self._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
7011
+ self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
6362
7012
  self.change_state(ProcessState.STOPPING)
6363
7013
 
6364
7014
  pid = self.pid
@@ -6403,7 +7053,7 @@ class ProcessImpl(Process):
6403
7053
 
6404
7054
  log.debug('sending %s (pid %s) sig %s', process_name, self.pid, sig_name(sig))
6405
7055
 
6406
- self._check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
7056
+ self.check_in_state(ProcessState.RUNNING, ProcessState.STARTING, ProcessState.STOPPING)
6407
7057
 
6408
7058
  try:
6409
7059
  try:
@@ -6432,7 +7082,7 @@ class ProcessImpl(Process):
6432
7082
  def finish(self, sts: int) -> None:
6433
7083
  """The process was reaped and we need to report and manage its state."""
6434
7084
 
6435
- self.drain()
7085
+ self._dispatchers.drain()
6436
7086
 
6437
7087
  es, msg = decode_wait_status(sts)
6438
7088
 
@@ -6463,7 +7113,7 @@ class ProcessImpl(Process):
6463
7113
  self._exitstatus = es
6464
7114
 
6465
7115
  fmt, args = 'stopped: %s (%s)', (process_name, msg)
6466
- self._check_in_state(ProcessState.STOPPING)
7116
+ self.check_in_state(ProcessState.STOPPING)
6467
7117
  self.change_state(ProcessState.STOPPED)
6468
7118
  if exit_expected:
6469
7119
  log.info(fmt, *args)
@@ -6474,7 +7124,7 @@ class ProcessImpl(Process):
6474
7124
  # the program did not stay up long enough to make it to RUNNING implies STARTING -> BACKOFF
6475
7125
  self._exitstatus = None
6476
7126
  self._spawn_err = 'Exited too quickly (process log may have details)'
6477
- self._check_in_state(ProcessState.STARTING)
7127
+ self.check_in_state(ProcessState.STARTING)
6478
7128
  self.change_state(ProcessState.BACKOFF)
6479
7129
  log.warning('exited: %s (%s)', process_name, msg + '; not expected')
6480
7130
 
@@ -6490,7 +7140,7 @@ class ProcessImpl(Process):
6490
7140
  if self._state == ProcessState.STARTING:
6491
7141
  self.change_state(ProcessState.RUNNING)
6492
7142
 
6493
- self._check_in_state(ProcessState.RUNNING)
7143
+ self.check_in_state(ProcessState.RUNNING)
6494
7144
 
6495
7145
  if exit_expected:
6496
7146
  # expected exit code
@@ -6504,19 +7154,8 @@ class ProcessImpl(Process):
6504
7154
 
6505
7155
  self._pid = 0
6506
7156
  close_parent_pipes(self._pipes)
6507
- self._pipes = {}
6508
- self._dispatchers = {}
6509
-
6510
- def set_uid(self) -> ta.Optional[str]:
6511
- if self._config.uid is None:
6512
- return None
6513
- msg = drop_privileges(self._config.uid)
6514
- return msg
6515
-
6516
- def __repr__(self) -> str:
6517
- # repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
6518
- name = self._config.name
6519
- return f'<Subprocess at {id(self)} with name {name} in state {self.get_state().name}>'
7157
+ self._pipes = ProcessPipes()
7158
+ self._dispatchers = Dispatchers([])
6520
7159
 
6521
7160
  def get_state(self) -> ProcessState:
6522
7161
  return self._state
@@ -6558,7 +7197,7 @@ class ProcessImpl(Process):
6558
7197
  # proc.config.startsecs,
6559
7198
  self._delay = 0
6560
7199
  self._backoff = 0
6561
- self._check_in_state(ProcessState.STARTING)
7200
+ self.check_in_state(ProcessState.STARTING)
6562
7201
  self.change_state(ProcessState.RUNNING)
6563
7202
  msg = ('entered RUNNING state, process has stayed up for > than %s seconds (startsecs)' % self._config.startsecs) # noqa
6564
7203
  logger.info('success: %s %s', process_name, msg)
@@ -6578,7 +7217,7 @@ class ProcessImpl(Process):
6578
7217
  log.warning('killing \'%s\' (%s) with SIGKILL', process_name, self.pid)
6579
7218
  self.kill(signal.SIGKILL)
6580
7219
 
6581
- def create_auto_child_logs(self) -> None:
7220
+ def after_setuid(self) -> None:
6582
7221
  # temporary logfiles which are erased at start time
6583
7222
  # get_autoname = self.context.get_auto_child_log_name # noqa
6584
7223
  # sid = self.context.config.identifier # noqa
@@ -6591,372 +7230,319 @@ class ProcessImpl(Process):
6591
7230
 
6592
7231
 
6593
7232
  ########################################
6594
- # ../supervisor.py
6595
-
6596
-
6597
- ##
6598
-
6599
-
6600
- class SignalHandler:
6601
- def __init__(
6602
- self,
6603
- *,
6604
- context: ServerContextImpl,
6605
- signal_receiver: SignalReceiver,
6606
- process_groups: ProcessGroups,
6607
- ) -> None:
6608
- super().__init__()
6609
-
6610
- self._context = context
6611
- self._signal_receiver = signal_receiver
6612
- self._process_groups = process_groups
6613
-
6614
- def set_signals(self) -> None:
6615
- self._signal_receiver.install(
6616
- signal.SIGTERM,
6617
- signal.SIGINT,
6618
- signal.SIGQUIT,
6619
- signal.SIGHUP,
6620
- signal.SIGCHLD,
6621
- signal.SIGUSR2,
6622
- )
6623
-
6624
- def handle_signals(self) -> None:
6625
- sig = self._signal_receiver.get_signal()
6626
- if not sig:
6627
- return
7233
+ # ../spawningimpl.py
6628
7234
 
6629
- if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
6630
- log.warning('received %s indicating exit request', sig_name(sig))
6631
- self._context.set_state(SupervisorState.SHUTDOWN)
6632
7235
 
6633
- elif sig == signal.SIGHUP:
6634
- if self._context.state == SupervisorState.SHUTDOWN:
6635
- log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
6636
- else:
6637
- log.warning('received %s indicating restart request', sig_name(sig)) # noqa
6638
- self._context.set_state(SupervisorState.RESTARTING)
7236
+ class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], int, OutputDispatcher]):
7237
+ pass
6639
7238
 
6640
- elif sig == signal.SIGCHLD:
6641
- log.debug('received %s indicating a child quit', sig_name(sig))
6642
7239
 
6643
- elif sig == signal.SIGUSR2:
6644
- log.info('received %s indicating log reopen request', sig_name(sig))
7240
+ class InputDispatcherFactory(Func3[Process, str, int, InputDispatcher]):
7241
+ pass
6645
7242
 
6646
- for group in self._process_groups:
6647
- group.reopen_logs()
6648
7243
 
6649
- else:
6650
- log.debug('received %s indicating nothing', sig_name(sig))
7244
+ InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
6651
7245
 
6652
7246
 
6653
7247
  ##
6654
7248
 
6655
7249
 
6656
- ProcessGroupFactory = ta.NewType('ProcessGroupFactory', Func[ProcessGroup]) # (config: ProcessGroupConfig)
6657
-
6658
-
6659
- class Supervisor:
7250
+ class ProcessSpawningImpl(ProcessSpawning):
6660
7251
  def __init__(
6661
7252
  self,
7253
+ process: Process,
6662
7254
  *,
6663
- context: ServerContextImpl,
6664
- poller: Poller,
6665
- process_groups: ProcessGroups,
6666
- signal_handler: SignalHandler,
6667
- event_callbacks: EventCallbacks,
6668
- process_group_factory: ProcessGroupFactory,
6669
- ) -> None:
6670
- super().__init__()
6671
-
6672
- self._context = context
6673
- self._poller = poller
6674
- self._process_groups = process_groups
6675
- self._signal_handler = signal_handler
6676
- self._event_callbacks = event_callbacks
6677
- self._process_group_factory = process_group_factory
6678
-
6679
- self._ticks: ta.Dict[int, float] = {}
6680
- self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
6681
- self._stopping = False # set after we detect that we are handling a stop request
6682
- self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
6683
-
6684
- #
6685
-
6686
- @property
6687
- def context(self) -> ServerContextImpl:
6688
- return self._context
6689
-
6690
- def get_state(self) -> SupervisorState:
6691
- return self._context.state
7255
+ server_config: ServerConfig,
7256
+ pid_history: PidHistory,
6692
7257
 
6693
- #
6694
-
6695
- class DiffToActive(ta.NamedTuple):
6696
- added: ta.List[ProcessGroupConfig]
6697
- changed: ta.List[ProcessGroupConfig]
6698
- removed: ta.List[ProcessGroupConfig]
6699
-
6700
- def diff_to_active(self) -> DiffToActive:
6701
- new = self._context.config.groups or []
6702
- cur = [group.config for group in self._process_groups]
7258
+ output_dispatcher_factory: OutputDispatcherFactory,
7259
+ input_dispatcher_factory: InputDispatcherFactory,
6703
7260
 
6704
- curdict = dict(zip([cfg.name for cfg in cur], cur))
6705
- newdict = dict(zip([cfg.name for cfg in new], new))
7261
+ inherited_fds: ta.Optional[InheritedFds] = None,
7262
+ ) -> None:
7263
+ super().__init__()
6706
7264
 
6707
- added = [cand for cand in new if cand.name not in curdict]
6708
- removed = [cand for cand in cur if cand.name not in newdict]
7265
+ self._process = process
6709
7266
 
6710
- changed = [cand for cand in new if cand != curdict.get(cand.name, cand)]
7267
+ self._server_config = server_config
7268
+ self._pid_history = pid_history
6711
7269
 
6712
- return Supervisor.DiffToActive(added, changed, removed)
7270
+ self._output_dispatcher_factory = output_dispatcher_factory
7271
+ self._input_dispatcher_factory = input_dispatcher_factory
6713
7272
 
6714
- def add_process_group(self, config: ProcessGroupConfig) -> bool:
6715
- if self._process_groups.get(config.name) is not None:
6716
- return False
7273
+ self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
6717
7274
 
6718
- group = self._process_group_factory(config)
6719
- group.after_setuid()
7275
+ #
6720
7276
 
6721
- self._process_groups.add(group)
7277
+ @property
7278
+ def process(self) -> Process:
7279
+ return self._process
6722
7280
 
6723
- return True
7281
+ @property
7282
+ def config(self) -> ProcessConfig:
7283
+ return self._process.config
6724
7284
 
6725
- def remove_process_group(self, name: str) -> bool:
6726
- if self._process_groups[name].get_unstopped_processes():
6727
- return False
7285
+ @property
7286
+ def group(self) -> ProcessGroup:
7287
+ return self._process.group
6728
7288
 
6729
- self._process_groups.remove(name)
7289
+ #
6730
7290
 
6731
- return True
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
6732
7296
 
6733
- def get_process_map(self) -> ta.Dict[int, Dispatcher]:
6734
- process_map: ta.Dict[int, Dispatcher] = {}
6735
- for group in self._process_groups:
6736
- process_map.update(group.get_dispatchers())
6737
- return process_map
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
6738
7307
 
6739
- def shutdown_report(self) -> ta.List[Process]:
6740
- unstopped: ta.List[Process] = []
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
6741
7313
 
6742
- for group in self._process_groups:
6743
- unstopped.extend(group.get_unstopped_processes())
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
6744
7326
 
6745
- if unstopped:
6746
- # throttle 'waiting for x to die' reports
6747
- now = time.time()
6748
- if now > (self._last_shutdown_report + 3): # every 3 secs
6749
- names = [as_string(p.config.name) for p in unstopped]
6750
- namestr = ', '.join(names)
6751
- log.info('waiting for %s to die', namestr)
6752
- self._last_shutdown_report = now
6753
- for proc in unstopped:
6754
- log.debug('%s state: %s', proc.config.name, proc.get_state().name)
7327
+ if pid != 0:
7328
+ sp = SpawnedProcess(
7329
+ pid,
7330
+ pipes,
7331
+ dispatchers,
7332
+ )
7333
+ self._spawn_as_parent(sp)
7334
+ return sp
6755
7335
 
6756
- return unstopped
7336
+ else:
7337
+ self._spawn_as_child(
7338
+ exe,
7339
+ argv,
7340
+ pipes,
7341
+ )
7342
+ raise RuntimeError('Unreachable') # noqa
6757
7343
 
6758
- #
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
+ """
6759
7349
 
6760
- def main(self) -> None:
6761
- self.setup()
6762
- self.run()
7350
+ try:
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
6763
7354
 
6764
- @cached_nullary
6765
- def setup(self) -> None:
6766
- if not self._context.first:
6767
- # prevent crash on libdispatch-based systems, at least for the first request
6768
- self._context.cleanup_fds()
7355
+ if args:
7356
+ program = args[0]
7357
+ else:
7358
+ raise BadCommandError('Command is empty')
6769
7359
 
6770
- self._context.set_uid_or_exit()
7360
+ if '/' in program:
7361
+ exe = program
7362
+ try:
7363
+ st = os.stat(exe)
7364
+ except OSError:
7365
+ st = None
6771
7366
 
6772
- if self._context.first:
6773
- self._context.set_rlimits_or_exit()
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
6774
7379
 
6775
- # this sets the options.logger object delay logger instantiation until after setuid
6776
- if not self._context.config.nocleanup:
6777
- # clean up old automatic logs
6778
- self._context.clear_auto_child_logdir()
7380
+ if st is None:
7381
+ exe = program
7382
+ else:
7383
+ exe = found # type: ignore
6779
7384
 
6780
- def run(
6781
- self,
6782
- *,
6783
- callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
6784
- ) -> None:
6785
- self._process_groups.clear()
6786
- self._stop_groups = None # clear
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))
6787
7414
 
6788
- self._event_callbacks.clear()
7415
+ return Dispatchers(dispatchers)
6789
7416
 
6790
- try:
6791
- for config in self._context.config.groups or []:
6792
- self.add_process_group(config)
7417
+ #
6793
7418
 
6794
- self._signal_handler.set_signals()
7419
+ def _spawn_as_parent(self, sp: SpawnedProcess) -> None:
7420
+ close_child_pipes(sp.pipes)
6795
7421
 
6796
- if not self._context.config.nodaemon and self._context.first:
6797
- self._context.daemonize()
7422
+ self._pid_history[sp.pid] = self.process
6798
7423
 
6799
- # writing pid file needs to come *after* daemonizing or pid will be wrong
6800
- self._context.write_pidfile()
7424
+ #
6801
7425
 
6802
- self._event_callbacks.notify(SupervisorRunningEvent())
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()
6803
7438
 
6804
- while True:
6805
- if callback is not None and not callback(self):
6806
- break
7439
+ #
6807
7440
 
6808
- self._run_once()
7441
+ # After preparation sending to fd 2 will put this output in the stderr log.
7442
+ self._prepare_child_fds(pipes)
6809
7443
 
6810
- finally:
6811
- self._context.cleanup()
7444
+ #
6812
7445
 
6813
- #
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)
6814
7452
 
6815
- def _run_once(self) -> None:
6816
- self._poll()
6817
- self._reap()
6818
- self._signal_handler.handle_signals()
6819
- self._tick()
7453
+ #
6820
7454
 
6821
- if self._context.state < SupervisorState.RUNNING:
6822
- self._ordered_stop_groups_phase_2()
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)
6823
7462
 
6824
- def _ordered_stop_groups_phase_1(self) -> None:
6825
- if self._stop_groups:
6826
- # stop the last group (the one with the "highest" priority)
6827
- self._stop_groups[-1].stop_all()
7463
+ #
6828
7464
 
6829
- def _ordered_stop_groups_phase_2(self) -> None:
6830
- # after phase 1 we've transitioned and reaped, let's see if we can remove the group we stopped from the
6831
- # stop_groups queue.
6832
- if self._stop_groups:
6833
- # pop the last group (the one with the "highest" priority)
6834
- group = self._stop_groups.pop()
6835
- if group.get_unstopped_processes():
6836
- # if any processes in the group aren't yet in a stopped state, we're not yet done shutting this group
6837
- # down, so push it back on to the end of the stop group queue
6838
- self._stop_groups.append(group)
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
6839
7474
 
6840
- def _poll(self) -> None:
6841
- combined_map = {}
6842
- combined_map.update(self.get_process_map())
7475
+ #
6843
7476
 
6844
- pgroups = list(self._process_groups)
6845
- pgroups.sort()
7477
+ try:
7478
+ if self.config.umask is not None:
7479
+ os.umask(self.config.umask)
7480
+ os.execve(exe, list(argv), env)
6846
7481
 
6847
- if self._context.state < SupervisorState.RUNNING:
6848
- if not self._stopping:
6849
- # first time, set the stopping flag, do a notification and set stop_groups
6850
- self._stopping = True
6851
- self._stop_groups = pgroups[:]
6852
- self._event_callbacks.notify(SupervisorStoppingEvent())
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))
6853
7486
 
6854
- self._ordered_stop_groups_phase_1()
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))
6855
7491
 
6856
- if not self.shutdown_report():
6857
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
6858
- raise ExitNow
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
6859
7495
 
6860
- for fd, dispatcher in combined_map.items():
6861
- if dispatcher.readable():
6862
- self._poller.register_readable(fd)
6863
- if dispatcher.writable():
6864
- self._poller.register_writable(fd)
7496
+ raise RuntimeError('Unreachable')
6865
7497
 
6866
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
6867
- r, w = self._poller.poll(timeout)
7498
+ def _prepare_child_fds(self, pipes: ProcessPipes) -> None:
7499
+ os.dup2(check_not_none(pipes.child_stdin), 0)
6868
7500
 
6869
- for fd in r:
6870
- if fd in combined_map:
6871
- try:
6872
- dispatcher = combined_map[fd]
6873
- log.debug('read event caused by %r', dispatcher)
6874
- dispatcher.handle_read_event()
6875
- if not dispatcher.readable():
6876
- self._poller.unregister_readable(fd)
6877
- except ExitNow:
6878
- raise
6879
- except Exception: # noqa
6880
- combined_map[fd].handle_error()
6881
- else:
6882
- # if the fd is not in combined_map, we should unregister it. otherwise, it will be polled every
6883
- # time, which may cause 100% cpu usage
6884
- log.debug('unexpected read event from fd %r', fd)
6885
- try:
6886
- self._poller.unregister_readable(fd)
6887
- except Exception: # noqa
6888
- pass
7501
+ os.dup2(check_not_none(pipes.child_stdout), 1)
6889
7502
 
6890
- for fd in w:
6891
- if fd in combined_map:
6892
- try:
6893
- dispatcher = combined_map[fd]
6894
- log.debug('write event caused by %r', dispatcher)
6895
- dispatcher.handle_write_event()
6896
- if not dispatcher.writable():
6897
- self._poller.unregister_writable(fd)
6898
- except ExitNow:
6899
- raise
6900
- except Exception: # noqa
6901
- combined_map[fd].handle_error()
6902
- else:
6903
- log.debug('unexpected write event from fd %r', fd)
6904
- try:
6905
- self._poller.unregister_writable(fd)
6906
- except Exception: # noqa
6907
- pass
7503
+ if self.config.redirect_stderr:
7504
+ os.dup2(check_not_none(pipes.child_stdout), 2)
7505
+ else:
7506
+ os.dup2(check_not_none(pipes.child_stderr), 2)
6908
7507
 
6909
- for group in pgroups:
6910
- group.transition()
7508
+ for i in range(3, self._server_config.minfds):
7509
+ if i in self._inherited_fds:
7510
+ continue
7511
+ close_fd(i)
6911
7512
 
6912
- def _reap(self, *, once: bool = False, depth: int = 0) -> None:
6913
- if depth >= 100:
6914
- return
7513
+ def _set_uid(self) -> ta.Optional[str]:
7514
+ if self.config.uid is None:
7515
+ return None
6915
7516
 
6916
- pid, sts = self._context.waitpid()
6917
- if not pid:
6918
- return
7517
+ msg = drop_privileges(self.config.uid)
7518
+ return msg
6919
7519
 
6920
- process = self._context.pid_history.get(pid, None)
6921
- if process is None:
6922
- _, msg = decode_wait_status(check_not_none(sts))
6923
- log.info('reaped unknown pid %s (%s)', pid, msg)
6924
- else:
6925
- process.finish(check_not_none(sts))
6926
- del self._context.pid_history[pid]
6927
7520
 
6928
- if not once:
6929
- # keep reaping until no more kids to reap, but don't recurse infinitely
6930
- self._reap(once=False, depth=depth + 1)
7521
+ ##
6931
7522
 
6932
- def _tick(self, now: ta.Optional[float] = None) -> None:
6933
- """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
6934
7523
 
6935
- if now is None:
6936
- # now won't be None in unit tests
6937
- now = time.time()
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}")
6938
7531
 
6939
- for event in TICK_EVENTS:
6940
- period = event.period
7532
+ elif stat.S_ISDIR(st[stat.ST_MODE]):
7533
+ raise NotExecutableError(f'Command at {exe!r} is a directory')
6941
7534
 
6942
- last_tick = self._ticks.get(period)
6943
- if last_tick is None:
6944
- # we just started up
6945
- last_tick = self._ticks[period] = timeslice(period, now)
7535
+ elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
7536
+ raise NotExecutableError(f'Command at {exe!r} is not executable')
6946
7537
 
6947
- this_tick = timeslice(period, now)
6948
- if this_tick != last_tick:
6949
- self._ticks[period] = this_tick
6950
- self._event_callbacks.notify(event(this_tick, self))
7538
+ elif not os.access(exe, os.X_OK):
7539
+ raise NoPermissionError(f'No permission to run command {exe!r}')
6951
7540
 
6952
7541
 
6953
7542
  ########################################
6954
7543
  # ../inject.py
6955
7544
 
6956
7545
 
6957
- ##
6958
-
6959
-
6960
7546
  def bind_server(
6961
7547
  config: ServerConfig,
6962
7548
  *,
@@ -6966,7 +7552,12 @@ def bind_server(
6966
7552
  lst: ta.List[InjectorBindingOrBindings] = [
6967
7553
  inj.bind(config),
6968
7554
 
6969
- 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),
6970
7561
 
6971
7562
  inj.bind(ServerContextImpl, singleton=True),
6972
7563
  inj.bind(ServerContext, to_key=ServerContextImpl),
@@ -6976,11 +7567,18 @@ def bind_server(
6976
7567
  inj.bind(SignalReceiver, singleton=True),
6977
7568
 
6978
7569
  inj.bind(SignalHandler, singleton=True),
6979
- inj.bind(ProcessGroups, singleton=True),
7570
+ inj.bind(ProcessGroupManager, singleton=True),
6980
7571
  inj.bind(Supervisor, singleton=True),
6981
7572
 
6982
- inj.bind_factory(ProcessGroupFactory, ProcessGroupImpl),
6983
- inj.bind_factory(ProcessFactory, ProcessImpl),
7573
+ inj.bind(PidHistory()),
7574
+
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),
6984
7582
  ]
6985
7583
 
6986
7584
  #
@@ -6992,6 +7590,16 @@ def bind_server(
6992
7590
 
6993
7591
  #
6994
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
+
6995
7603
  return inj.as_bindings(*lst)
6996
7604
 
6997
7605