ominfra 0.0.0.dev119__py3-none-any.whl → 0.0.0.dev121__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/journald2aws/__main__.py +4 -0
- ominfra/clouds/aws/journald2aws/driver.py +34 -13
- ominfra/clouds/aws/journald2aws/main.py +2 -5
- ominfra/configs.py +70 -0
- ominfra/deploy/_executor.py +1 -1
- ominfra/deploy/poly/_main.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +994 -26
- ominfra/scripts/supervisor.py +1969 -262
- ominfra/supervisor/compat.py +13 -0
- ominfra/supervisor/configs.py +21 -0
- ominfra/supervisor/context.py +13 -2
- ominfra/supervisor/dispatchers.py +4 -4
- ominfra/supervisor/events.py +4 -7
- ominfra/supervisor/main.py +82 -11
- ominfra/supervisor/process.py +46 -10
- ominfra/supervisor/supervisor.py +118 -88
- ominfra/supervisor/types.py +5 -0
- ominfra/threadworkers.py +66 -9
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/RECORD +25 -23
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev121.dist-info}/top_level.txt +0 -0
    
        ominfra/supervisor/compat.py
    CHANGED
    
    | @@ -161,6 +161,19 @@ def close_fd(fd: int) -> bool: | |
| 161 161 | 
             
                return True
         | 
| 162 162 |  | 
| 163 163 |  | 
| 164 | 
            +
            def is_fd_open(fd: int) -> bool:
         | 
| 165 | 
            +
                try:
         | 
| 166 | 
            +
                    n = os.dup(fd)
         | 
| 167 | 
            +
                except OSError:
         | 
| 168 | 
            +
                    return False
         | 
| 169 | 
            +
                os.close(n)
         | 
| 170 | 
            +
                return True
         | 
| 171 | 
            +
             | 
| 172 | 
            +
             | 
| 173 | 
            +
            def get_open_fds(limit: int) -> ta.FrozenSet[int]:
         | 
| 174 | 
            +
                return frozenset(filter(is_fd_open, range(limit)))
         | 
| 175 | 
            +
             | 
| 176 | 
            +
             | 
| 164 177 | 
             
            def mktempfile(suffix: str, prefix: str, dir: str) -> str:  # noqa
         | 
| 165 178 | 
             
                fd, filename = tempfile.mkstemp(suffix, prefix, dir)
         | 
| 166 179 | 
             
                os.close(fd)
         | 
    
        ominfra/supervisor/configs.py
    CHANGED
    
    | @@ -5,6 +5,8 @@ import signal | |
| 5 5 | 
             
            import tempfile
         | 
| 6 6 | 
             
            import typing as ta
         | 
| 7 7 |  | 
| 8 | 
            +
            from ..configs import ConfigMapping
         | 
| 9 | 
            +
            from ..configs import build_config_named_children
         | 
| 8 10 | 
             
            from .datatypes import byte_size
         | 
| 9 11 | 
             
            from .datatypes import existing_directory
         | 
| 10 12 | 
             
            from .datatypes import existing_dirpath
         | 
| @@ -12,6 +14,9 @@ from .datatypes import logging_level | |
| 12 14 | 
             
            from .datatypes import octal_type
         | 
| 13 15 |  | 
| 14 16 |  | 
| 17 | 
            +
            ##
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 15 20 | 
             
            @dc.dataclass(frozen=True)
         | 
| 16 21 | 
             
            class ProcessConfig:
         | 
| 17 22 | 
             
                name: str
         | 
| @@ -108,3 +113,19 @@ class ServerConfig: | |
| 108 113 | 
             
                        child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
         | 
| 109 114 | 
             
                        **kwargs,
         | 
| 110 115 | 
             
                    )
         | 
| 116 | 
            +
             | 
| 117 | 
            +
             | 
| 118 | 
            +
            ##
         | 
| 119 | 
            +
             | 
| 120 | 
            +
             | 
| 121 | 
            +
            def prepare_process_group_config(dct: ConfigMapping) -> ConfigMapping:
         | 
| 122 | 
            +
                out = dict(dct)
         | 
| 123 | 
            +
                out['processes'] = build_config_named_children(out.get('processes'))
         | 
| 124 | 
            +
                return out
         | 
| 125 | 
            +
             | 
| 126 | 
            +
             | 
| 127 | 
            +
            def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
         | 
| 128 | 
            +
                out = dict(dct)
         | 
| 129 | 
            +
                group_dcts = build_config_named_children(out.get('groups'))
         | 
| 130 | 
            +
                out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
         | 
| 131 | 
            +
                return out
         | 
    
        ominfra/supervisor/context.py
    CHANGED
    
    | @@ -32,17 +32,24 @@ from .types import AbstractServerContext | |
| 32 32 | 
             
            from .types import AbstractSubprocess
         | 
| 33 33 |  | 
| 34 34 |  | 
| 35 | 
            +
            ServerEpoch = ta.NewType('ServerEpoch', int)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
         | 
| 38 | 
            +
             | 
| 39 | 
            +
             | 
| 35 40 | 
             
            class ServerContext(AbstractServerContext):
         | 
| 36 41 | 
             
                def __init__(
         | 
| 37 42 | 
             
                        self,
         | 
| 38 43 | 
             
                        config: ServerConfig,
         | 
| 39 44 | 
             
                        *,
         | 
| 40 | 
            -
                        epoch:  | 
| 45 | 
            +
                        epoch: ServerEpoch = ServerEpoch(0),
         | 
| 46 | 
            +
                        inherited_fds: ta.Optional[InheritedFds] = None,
         | 
| 41 47 | 
             
                ) -> None:
         | 
| 42 48 | 
             
                    super().__init__()
         | 
| 43 49 |  | 
| 44 50 | 
             
                    self._config = config
         | 
| 45 51 | 
             
                    self._epoch = epoch
         | 
| 52 | 
            +
                    self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
         | 
| 46 53 |  | 
| 47 54 | 
             
                    self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
         | 
| 48 55 | 
             
                    self._state: SupervisorState = SupervisorStates.RUNNING
         | 
| @@ -66,7 +73,7 @@ class ServerContext(AbstractServerContext): | |
| 66 73 | 
             
                    return self._config
         | 
| 67 74 |  | 
| 68 75 | 
             
                @property
         | 
| 69 | 
            -
                def epoch(self) ->  | 
| 76 | 
            +
                def epoch(self) -> ServerEpoch:
         | 
| 70 77 | 
             
                    return self._epoch
         | 
| 71 78 |  | 
| 72 79 | 
             
                @property
         | 
| @@ -96,6 +103,10 @@ class ServerContext(AbstractServerContext): | |
| 96 103 | 
             
                def gid(self) -> ta.Optional[int]:
         | 
| 97 104 | 
             
                    return self._gid
         | 
| 98 105 |  | 
| 106 | 
            +
                @property
         | 
| 107 | 
            +
                def inherited_fds(self) -> InheritedFds:
         | 
| 108 | 
            +
                    return self._inherited_fds
         | 
| 109 | 
            +
             | 
| 99 110 | 
             
                ##
         | 
| 100 111 |  | 
| 101 112 | 
             
                def set_signals(self) -> None:
         | 
| @@ -13,9 +13,9 @@ from .compat import find_prefix_at_end | |
| 13 13 | 
             
            from .compat import readfd
         | 
| 14 14 | 
             
            from .compat import strip_escapes
         | 
| 15 15 | 
             
            from .configs import ProcessConfig
         | 
| 16 | 
            +
            from .events import EVENT_CALLBACKS
         | 
| 16 17 | 
             
            from .events import ProcessLogStderrEvent
         | 
| 17 18 | 
             
            from .events import ProcessLogStdoutEvent
         | 
| 18 | 
            -
            from .events import notify_event
         | 
| 19 19 | 
             
            from .types import AbstractSubprocess
         | 
| 20 20 |  | 
| 21 21 |  | 
| @@ -207,10 +207,10 @@ class OutputDispatcher(Dispatcher): | |
| 207 207 |  | 
| 208 208 | 
             
                        if self._channel == 'stdout':
         | 
| 209 209 | 
             
                            if self._stdout_events_enabled:
         | 
| 210 | 
            -
                                 | 
| 210 | 
            +
                                EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
         | 
| 211 211 |  | 
| 212 212 | 
             
                        elif self._stderr_events_enabled:
         | 
| 213 | 
            -
                             | 
| 213 | 
            +
                            EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
         | 
| 214 214 |  | 
| 215 215 | 
             
                def record_output(self):
         | 
| 216 216 | 
             
                    if self._capture_log is None:
         | 
| @@ -261,7 +261,7 @@ class OutputDispatcher(Dispatcher): | |
| 261 261 | 
             
                            channel = self._channel
         | 
| 262 262 | 
             
                            procname = self._process.config.name
         | 
| 263 263 | 
             
                            event = self.event_type(self._process, self._process.pid, data)
         | 
| 264 | 
            -
                             | 
| 264 | 
            +
                            EVENT_CALLBACKS.notify(event)
         | 
| 265 265 |  | 
| 266 266 | 
             
                            log.debug('%r %s emitted a comm event', procname, channel)
         | 
| 267 267 | 
             
                            for handler in self._capture_log.handlers:
         | 
    
        ominfra/supervisor/events.py
    CHANGED
    
    | @@ -29,12 +29,9 @@ class EventCallbacks: | |
| 29 29 |  | 
| 30 30 | 
             
            EVENT_CALLBACKS = EventCallbacks()
         | 
| 31 31 |  | 
| 32 | 
            -
            notify_event = EVENT_CALLBACKS.notify
         | 
| 33 | 
            -
            clear_events = EVENT_CALLBACKS.clear
         | 
| 34 | 
            -
             | 
| 35 32 |  | 
| 36 33 | 
             
            class Event(abc.ABC):  # noqa
         | 
| 37 | 
            -
                """Abstract event type | 
| 34 | 
            +
                """Abstract event type."""
         | 
| 38 35 |  | 
| 39 36 |  | 
| 40 37 | 
             
            class ProcessLogEvent(Event, abc.ABC):
         | 
| @@ -114,7 +111,7 @@ class RemoteCommunicationEvent(Event): | |
| 114 111 |  | 
| 115 112 |  | 
| 116 113 | 
             
            class SupervisorStateChangeEvent(Event):
         | 
| 117 | 
            -
                """ | 
| 114 | 
            +
                """Abstract class."""
         | 
| 118 115 |  | 
| 119 116 | 
             
                def payload(self):
         | 
| 120 117 | 
             
                    return ''
         | 
| @@ -136,7 +133,7 @@ class EventRejectedEvent:  # purposely does not subclass Event | |
| 136 133 |  | 
| 137 134 |  | 
| 138 135 | 
             
            class ProcessStateEvent(Event):
         | 
| 139 | 
            -
                """ | 
| 136 | 
            +
                """Abstract class, never raised directly."""
         | 
| 140 137 | 
             
                frm = None
         | 
| 141 138 | 
             
                to = None
         | 
| 142 139 |  | 
| @@ -225,7 +222,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent): | |
| 225 222 |  | 
| 226 223 |  | 
| 227 224 | 
             
            class TickEvent(Event):
         | 
| 228 | 
            -
                """ | 
| 225 | 
            +
                """Abstract."""
         | 
| 229 226 |  | 
| 230 227 | 
             
                def __init__(self, when, supervisord):
         | 
| 231 228 | 
             
                    super().__init__()
         | 
    
        ominfra/supervisor/main.py
    CHANGED
    
    | @@ -1,19 +1,82 @@ | |
| 1 1 | 
             
            #!/usr/bin/env python3
         | 
| 2 2 | 
             
            # ruff: noqa: UP006 UP007
         | 
| 3 3 | 
             
            # @omlish-amalg ../scripts/supervisor.py
         | 
| 4 | 
            +
            import functools
         | 
| 4 5 | 
             
            import itertools
         | 
| 5 | 
            -
            import  | 
| 6 | 
            +
            import os.path
         | 
| 6 7 | 
             
            import typing as ta
         | 
| 7 8 |  | 
| 9 | 
            +
            from omlish.lite.inject import Injector
         | 
| 10 | 
            +
            from omlish.lite.inject import InjectorBindingOrBindings
         | 
| 11 | 
            +
            from omlish.lite.inject import InjectorBindings
         | 
| 12 | 
            +
            from omlish.lite.inject import inj
         | 
| 8 13 | 
             
            from omlish.lite.journald import journald_log_handler_factory
         | 
| 9 14 | 
             
            from omlish.lite.logs import configure_standard_logging
         | 
| 10 | 
            -
            from omlish.lite.marshal import unmarshal_obj
         | 
| 11 15 |  | 
| 16 | 
            +
            from ..configs import read_config_file
         | 
| 12 17 | 
             
            from .compat import ExitNow
         | 
| 18 | 
            +
            from .compat import get_open_fds
         | 
| 19 | 
            +
            from .configs import ProcessConfig
         | 
| 20 | 
            +
            from .configs import ProcessGroupConfig
         | 
| 13 21 | 
             
            from .configs import ServerConfig
         | 
| 22 | 
            +
            from .configs import prepare_server_config
         | 
| 23 | 
            +
            from .context import InheritedFds
         | 
| 14 24 | 
             
            from .context import ServerContext
         | 
| 25 | 
            +
            from .context import ServerEpoch
         | 
| 26 | 
            +
            from .process import ProcessGroup
         | 
| 27 | 
            +
            from .process import Subprocess
         | 
| 28 | 
            +
            from .process import SubprocessFactory
         | 
| 15 29 | 
             
            from .states import SupervisorStates
         | 
| 30 | 
            +
            from .supervisor import ProcessGroupFactory
         | 
| 16 31 | 
             
            from .supervisor import Supervisor
         | 
| 32 | 
            +
            from .types import AbstractServerContext
         | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
            ##
         | 
| 36 | 
            +
             | 
| 37 | 
            +
             | 
| 38 | 
            +
            def build_server_bindings(
         | 
| 39 | 
            +
                    config: ServerConfig,
         | 
| 40 | 
            +
                    *,
         | 
| 41 | 
            +
                    server_epoch: ta.Optional[ServerEpoch] = None,
         | 
| 42 | 
            +
                    inherited_fds: ta.Optional[InheritedFds] = None,
         | 
| 43 | 
            +
            ) -> InjectorBindings:
         | 
| 44 | 
            +
                lst: ta.List[InjectorBindingOrBindings] = [
         | 
| 45 | 
            +
                    inj.bind(config),
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    inj.bind(ServerContext, singleton=True),
         | 
| 48 | 
            +
                    inj.bind(AbstractServerContext, to_key=ServerContext),
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    inj.bind(Supervisor, singleton=True),
         | 
| 51 | 
            +
                ]
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def make_process_group_factory(injector: Injector) -> ProcessGroupFactory:
         | 
| 56 | 
            +
                    def inner(group_config: ProcessGroupConfig) -> ProcessGroup:
         | 
| 57 | 
            +
                        return injector.inject(functools.partial(ProcessGroup, group_config))
         | 
| 58 | 
            +
                    return ProcessGroupFactory(inner)
         | 
| 59 | 
            +
                lst.append(inj.bind(make_process_group_factory))
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def make_subprocess_factory(injector: Injector) -> SubprocessFactory:
         | 
| 62 | 
            +
                    def inner(process_config: ProcessConfig, group: ProcessGroup) -> Subprocess:
         | 
| 63 | 
            +
                        return injector.inject(functools.partial(Subprocess, process_config, group))
         | 
| 64 | 
            +
                    return SubprocessFactory(inner)
         | 
| 65 | 
            +
                lst.append(inj.bind(make_subprocess_factory))
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                #
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                if server_epoch is not None:
         | 
| 70 | 
            +
                    lst.append(inj.bind(server_epoch, key=ServerEpoch))
         | 
| 71 | 
            +
                if inherited_fds is not None:
         | 
| 72 | 
            +
                    lst.append(inj.bind(inherited_fds, key=InheritedFds))
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                return inj.as_bindings(*lst)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
             | 
| 79 | 
            +
            ##
         | 
| 17 80 |  | 
| 18 81 |  | 
| 19 82 | 
             
            def main(
         | 
| @@ -26,6 +89,7 @@ def main( | |
| 26 89 | 
             
                parser = argparse.ArgumentParser()
         | 
| 27 90 | 
             
                parser.add_argument('config_file', metavar='config-file')
         | 
| 28 91 | 
             
                parser.add_argument('--no-journald', action='store_true')
         | 
| 92 | 
            +
                parser.add_argument('--inherit-initial-fds', action='store_true')
         | 
| 29 93 | 
             
                args = parser.parse_args(argv)
         | 
| 30 94 |  | 
| 31 95 | 
             
                #
         | 
| @@ -41,20 +105,27 @@ def main( | |
| 41 105 |  | 
| 42 106 | 
             
                #
         | 
| 43 107 |  | 
| 108 | 
            +
                initial_fds: ta.Optional[InheritedFds] = None
         | 
| 109 | 
            +
                if args.inherit_initial_fds:
         | 
| 110 | 
            +
                    initial_fds = InheritedFds(get_open_fds(0x10000))
         | 
| 111 | 
            +
             | 
| 44 112 | 
             
                # if we hup, restart by making a new Supervisor()
         | 
| 45 113 | 
             
                for epoch in itertools.count():
         | 
| 46 | 
            -
                     | 
| 47 | 
            -
                         | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
                     | 
| 114 | 
            +
                    config = read_config_file(
         | 
| 115 | 
            +
                        os.path.expanduser(cf),
         | 
| 116 | 
            +
                        ServerConfig,
         | 
| 117 | 
            +
                        prepare=prepare_server_config,
         | 
| 118 | 
            +
                    )
         | 
| 51 119 |  | 
| 52 | 
            -
                     | 
| 120 | 
            +
                    injector = inj.create_injector(build_server_bindings(
         | 
| 53 121 | 
             
                        config,
         | 
| 54 | 
            -
                         | 
| 55 | 
            -
             | 
| 122 | 
            +
                        server_epoch=ServerEpoch(epoch),
         | 
| 123 | 
            +
                        inherited_fds=initial_fds,
         | 
| 124 | 
            +
                    ))
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    context = injector.provide(ServerContext)
         | 
| 127 | 
            +
                    supervisor = injector.provide(Supervisor)
         | 
| 56 128 |  | 
| 57 | 
            -
                    supervisor = Supervisor(context)
         | 
| 58 129 | 
             
                    try:
         | 
| 59 130 | 
             
                        supervisor.main()
         | 
| 60 131 | 
             
                    except ExitNow:
         | 
    
        ominfra/supervisor/process.py
    CHANGED
    
    | @@ -1,7 +1,8 @@ | |
| 1 1 | 
             
            # ruff: noqa: UP006 UP007
         | 
| 2 | 
            +
            import dataclasses as dc
         | 
| 2 3 | 
             
            import errno
         | 
| 3 4 | 
             
            import functools
         | 
| 4 | 
            -
            import os
         | 
| 5 | 
            +
            import os.path
         | 
| 5 6 | 
             
            import shlex
         | 
| 6 7 | 
             
            import signal
         | 
| 7 8 | 
             
            import time
         | 
| @@ -30,6 +31,7 @@ from .datatypes import RestartUnconditionally | |
| 30 31 | 
             
            from .dispatchers import Dispatcher
         | 
| 31 32 | 
             
            from .dispatchers import InputDispatcher
         | 
| 32 33 | 
             
            from .dispatchers import OutputDispatcher
         | 
| 34 | 
            +
            from .events import EVENT_CALLBACKS
         | 
| 33 35 | 
             
            from .events import EventRejectedEvent
         | 
| 34 36 | 
             
            from .events import ProcessCommunicationEvent
         | 
| 35 37 | 
             
            from .events import ProcessCommunicationStderrEvent
         | 
| @@ -43,7 +45,6 @@ from .events import ProcessStateStartingEvent | |
| 43 45 | 
             
            from .events import ProcessStateStoppedEvent
         | 
| 44 46 | 
             
            from .events import ProcessStateStoppingEvent
         | 
| 45 47 | 
             
            from .events import ProcessStateUnknownEvent
         | 
| 46 | 
            -
            from .events import notify_event
         | 
| 47 48 | 
             
            from .exceptions import BadCommandError
         | 
| 48 49 | 
             
            from .exceptions import ProcessError
         | 
| 49 50 | 
             
            from .states import STOPPED_STATES
         | 
| @@ -55,6 +56,9 @@ from .types import AbstractServerContext | |
| 55 56 | 
             
            from .types import AbstractSubprocess
         | 
| 56 57 |  | 
| 57 58 |  | 
| 59 | 
            +
            ##
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 58 62 | 
             
            @functools.total_ordering
         | 
| 59 63 | 
             
            class Subprocess(AbstractSubprocess):
         | 
| 60 64 | 
             
                """A class to manage a subprocess."""
         | 
| @@ -80,7 +84,12 @@ class Subprocess(AbstractSubprocess): | |
| 80 84 | 
             
                spawn_err = None  # error message attached by spawn() if any
         | 
| 81 85 | 
             
                group = None  # ProcessGroup instance if process is in the group
         | 
| 82 86 |  | 
| 83 | 
            -
                def __init__( | 
| 87 | 
            +
                def __init__(
         | 
| 88 | 
            +
                        self,
         | 
| 89 | 
            +
                        config: ProcessConfig,
         | 
| 90 | 
            +
                        group: 'ProcessGroup',
         | 
| 91 | 
            +
                        context: AbstractServerContext,
         | 
| 92 | 
            +
                ) -> None:
         | 
| 84 93 | 
             
                    super().__init__()
         | 
| 85 94 | 
             
                    self._config = config
         | 
| 86 95 | 
             
                    self.group = group
         | 
| @@ -207,7 +216,7 @@ class Subprocess(AbstractSubprocess): | |
| 207 216 | 
             
                    event_class = self.event_map.get(new_state)
         | 
| 208 217 | 
             
                    if event_class is not None:
         | 
| 209 218 | 
             
                        event = event_class(self, old_state, expected)
         | 
| 210 | 
            -
                         | 
| 219 | 
            +
                        EVENT_CALLBACKS.notify(event)
         | 
| 211 220 |  | 
| 212 221 | 
             
                    return True
         | 
| 213 222 |  | 
| @@ -324,7 +333,10 @@ class Subprocess(AbstractSubprocess): | |
| 324 333 | 
             
                        os.dup2(self._pipes['child_stdout'], 2)
         | 
| 325 334 | 
             
                    else:
         | 
| 326 335 | 
             
                        os.dup2(self._pipes['child_stderr'], 2)
         | 
| 336 | 
            +
             | 
| 327 337 | 
             
                    for i in range(3, self.context.config.minfds):
         | 
| 338 | 
            +
                        if i in self.context.inherited_fds:
         | 
| 339 | 
            +
                            continue
         | 
| 328 340 | 
             
                        close_fd(i)
         | 
| 329 341 |  | 
| 330 342 | 
             
                def _spawn_as_child(self, filename: str, argv: ta.Sequence[str]) -> None:
         | 
| @@ -359,7 +371,7 @@ class Subprocess(AbstractSubprocess): | |
| 359 371 | 
             
                        cwd = self.config.directory
         | 
| 360 372 | 
             
                        try:
         | 
| 361 373 | 
             
                            if cwd is not None:
         | 
| 362 | 
            -
                                os.chdir(cwd)
         | 
| 374 | 
            +
                                os.chdir(os.path.expanduser(cwd))
         | 
| 363 375 | 
             
                        except OSError as why:
         | 
| 364 376 | 
             
                            code = errno.errorcode.get(why.args[0], why.args[0])
         | 
| 365 377 | 
             
                            msg = f"couldn't chdir to {cwd}: {code}\n"
         | 
| @@ -415,7 +427,7 @@ class Subprocess(AbstractSubprocess): | |
| 415 427 | 
             
                    return self.kill(self.config.stopsignal)
         | 
| 416 428 |  | 
| 417 429 | 
             
                def stop_report(self) -> None:
         | 
| 418 | 
            -
                    """ | 
| 430 | 
            +
                    """Log a 'waiting for x to stop' message with throttling."""
         | 
| 419 431 | 
             
                    if self.state == ProcessStates.STOPPING:
         | 
| 420 432 | 
             
                        now = time.time()
         | 
| 421 433 |  | 
| @@ -545,7 +557,7 @@ class Subprocess(AbstractSubprocess): | |
| 545 557 | 
             
                    return None
         | 
| 546 558 |  | 
| 547 559 | 
             
                def finish(self, sts: int) -> None:
         | 
| 548 | 
            -
                    """ | 
| 560 | 
            +
                    """The process was reaped and we need to report and manage its state."""
         | 
| 549 561 |  | 
| 550 562 | 
             
                    self.drain()
         | 
| 551 563 |  | 
| @@ -626,7 +638,7 @@ class Subprocess(AbstractSubprocess): | |
| 626 638 | 
             
                    # system that this event was rejected so it can be processed again.
         | 
| 627 639 | 
             
                    if self.event is not None:
         | 
| 628 640 | 
             
                        # Note: this should only be true if we were in the BUSY state when finish() was called.
         | 
| 629 | 
            -
                         | 
| 641 | 
            +
                        EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event))  # type: ignore
         | 
| 630 642 | 
             
                        self.event = None
         | 
| 631 643 |  | 
| 632 644 | 
             
                def set_uid(self) -> ta.Optional[str]:
         | 
| @@ -718,15 +730,39 @@ class Subprocess(AbstractSubprocess): | |
| 718 730 | 
             
                    pass
         | 
| 719 731 |  | 
| 720 732 |  | 
| 733 | 
            +
            ##
         | 
| 734 | 
            +
             | 
| 735 | 
            +
             | 
| 736 | 
            +
            @dc.dataclass(frozen=True)
         | 
| 737 | 
            +
            class SubprocessFactory:
         | 
| 738 | 
            +
                fn: ta.Callable[[ProcessConfig, 'ProcessGroup'], Subprocess]
         | 
| 739 | 
            +
             | 
| 740 | 
            +
                def __call__(self, config: ProcessConfig, group: 'ProcessGroup') -> Subprocess:
         | 
| 741 | 
            +
                    return self.fn(config, group)
         | 
| 742 | 
            +
             | 
| 743 | 
            +
             | 
| 721 744 | 
             
            @functools.total_ordering
         | 
| 722 745 | 
             
            class ProcessGroup:
         | 
| 723 | 
            -
                def __init__( | 
| 746 | 
            +
                def __init__(
         | 
| 747 | 
            +
                        self,
         | 
| 748 | 
            +
                        config: ProcessGroupConfig,
         | 
| 749 | 
            +
                        context: ServerContext,
         | 
| 750 | 
            +
                        *,
         | 
| 751 | 
            +
                        subprocess_factory: ta.Optional[SubprocessFactory] = None,
         | 
| 752 | 
            +
                ):
         | 
| 724 753 | 
             
                    super().__init__()
         | 
| 725 754 | 
             
                    self.config = config
         | 
| 726 755 | 
             
                    self.context = context
         | 
| 756 | 
            +
             | 
| 757 | 
            +
                    if subprocess_factory is None:
         | 
| 758 | 
            +
                        def make_subprocess(config: ProcessConfig, group: ProcessGroup) -> Subprocess:
         | 
| 759 | 
            +
                            return Subprocess(config, group, self.context)
         | 
| 760 | 
            +
                        subprocess_factory = SubprocessFactory(make_subprocess)
         | 
| 761 | 
            +
                    self._subprocess_factory = subprocess_factory
         | 
| 762 | 
            +
             | 
| 727 763 | 
             
                    self.processes = {}
         | 
| 728 764 | 
             
                    for pconfig in self.config.processes or []:
         | 
| 729 | 
            -
                        process =  | 
| 765 | 
            +
                        process = self._subprocess_factory(pconfig, self)
         | 
| 730 766 | 
             
                        self.processes[pconfig.name] = process
         | 
| 731 767 |  | 
| 732 768 | 
             
                def __lt__(self, other):
         |