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