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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. ominfra/clouds/aws/auth.py +1 -1
  2. ominfra/deploy/_executor.py +1 -1
  3. ominfra/deploy/poly/_main.py +1 -1
  4. ominfra/pyremote/_runcommands.py +1 -1
  5. ominfra/scripts/journald2aws.py +2 -2
  6. ominfra/scripts/supervisor.py +1825 -1217
  7. ominfra/supervisor/collections.py +52 -0
  8. ominfra/supervisor/context.py +2 -336
  9. ominfra/supervisor/datatypes.py +1 -63
  10. ominfra/supervisor/dispatchers.py +22 -338
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -110
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +45 -13
  15. ominfra/supervisor/main.py +1 -1
  16. ominfra/supervisor/pipes.py +83 -0
  17. ominfra/supervisor/poller.py +6 -3
  18. ominfra/supervisor/privileges.py +65 -0
  19. ominfra/supervisor/processes.py +18 -0
  20. ominfra/supervisor/{process.py → processesimpl.py} +99 -317
  21. ominfra/supervisor/setup.py +38 -0
  22. ominfra/supervisor/setupimpl.py +261 -0
  23. ominfra/supervisor/signals.py +24 -16
  24. ominfra/supervisor/spawning.py +31 -0
  25. ominfra/supervisor/spawningimpl.py +347 -0
  26. ominfra/supervisor/supervisor.py +54 -78
  27. ominfra/supervisor/types.py +122 -39
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
@@ -1,113 +1,16 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import typing as ta
3
3
 
4
- from omlish.lite.typing import Func
5
-
4
+ from .collections import KeyedCollectionAccessors
6
5
  from .configs import ProcessGroupConfig
7
- from .dispatchers import Dispatcher
8
6
  from .events import EventCallbacks
9
7
  from .events import ProcessGroupAddedEvent
10
8
  from .events import ProcessGroupRemovedEvent
11
- from .states import ProcessState
12
9
  from .types import Process
13
10
  from .types import ProcessGroup
14
- from .types import ServerContext
15
-
16
-
17
- ##
18
11
 
19
12
 
20
- ProcessFactory = ta.NewType('ProcessFactory', Func[Process]) # (config: ProcessConfig, group: ProcessGroup)
21
-
22
-
23
- class ProcessGroupImpl(ProcessGroup):
24
- def __init__(
25
- self,
26
- config: ProcessGroupConfig,
27
- context: ServerContext,
28
- *,
29
- process_factory: ProcessFactory,
30
- ):
31
- super().__init__()
32
-
33
- self._config = config
34
- self._context = context
35
- self._process_factory = process_factory
36
-
37
- self._processes = {}
38
- for pconfig in self._config.processes or []:
39
- process = self._process_factory(pconfig, self)
40
- self._processes[pconfig.name] = process
41
-
42
- @property
43
- def config(self) -> ProcessGroupConfig:
44
- return self._config
45
-
46
- @property
47
- def name(self) -> str:
48
- return self._config.name
49
-
50
- @property
51
- def context(self) -> ServerContext:
52
- return self._context
53
-
54
- def __repr__(self):
55
- # repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
56
- name = self._config.name
57
- return f'<{self.__class__.__name__} instance at {id(self)} named {name}>'
58
-
59
- def remove_logs(self) -> None:
60
- for process in self._processes.values():
61
- process.remove_logs()
62
-
63
- def reopen_logs(self) -> None:
64
- for process in self._processes.values():
65
- process.reopen_logs()
66
-
67
- def stop_all(self) -> None:
68
- processes = list(self._processes.values())
69
- processes.sort()
70
- processes.reverse() # stop in desc priority order
71
-
72
- for proc in processes:
73
- state = proc.get_state()
74
- if state == ProcessState.RUNNING:
75
- # RUNNING -> STOPPING
76
- proc.stop()
77
-
78
- elif state == ProcessState.STARTING:
79
- # STARTING -> STOPPING
80
- proc.stop()
81
-
82
- elif state == ProcessState.BACKOFF:
83
- # BACKOFF -> FATAL
84
- proc.give_up()
85
-
86
- def get_unstopped_processes(self) -> ta.List[Process]:
87
- return [x for x in self._processes.values() if not x.get_state().stopped]
88
-
89
- def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
90
- dispatchers: dict = {}
91
- for process in self._processes.values():
92
- dispatchers.update(process.get_dispatchers())
93
- return dispatchers
94
-
95
- def before_remove(self) -> None:
96
- pass
97
-
98
- def transition(self) -> None:
99
- for proc in self._processes.values():
100
- proc.transition()
101
-
102
- def after_setuid(self) -> None:
103
- for proc in self._processes.values():
104
- proc.create_auto_child_logs()
105
-
106
-
107
- ##
108
-
109
-
110
- class ProcessGroups:
13
+ class ProcessGroupManager(KeyedCollectionAccessors[str, ProcessGroup]):
111
14
  def __init__(
112
15
  self,
113
16
  *,
@@ -119,20 +22,17 @@ class ProcessGroups:
119
22
 
120
23
  self._by_name: ta.Dict[str, ProcessGroup] = {}
121
24
 
122
- def get(self, name: str) -> ta.Optional[ProcessGroup]:
123
- return self._by_name.get(name)
25
+ @property
26
+ def _by_key(self) -> ta.Mapping[str, ProcessGroup]:
27
+ return self._by_name
124
28
 
125
- def __getitem__(self, name: str) -> ProcessGroup:
126
- return self._by_name[name]
29
+ #
127
30
 
128
- def __len__(self) -> int:
129
- return len(self._by_name)
31
+ def all_processes(self) -> ta.Iterator[Process]:
32
+ for g in self:
33
+ yield from g
130
34
 
131
- def __iter__(self) -> ta.Iterator[ProcessGroup]:
132
- return iter(self._by_name.values())
133
-
134
- def all(self) -> ta.Mapping[str, ProcessGroup]:
135
- return self._by_name
35
+ #
136
36
 
137
37
  def add(self, group: ProcessGroup) -> None:
138
38
  if (name := group.name) in self._by_name:
@@ -154,3 +54,26 @@ class ProcessGroups:
154
54
  def clear(self) -> None:
155
55
  # FIXME: events?
156
56
  self._by_name.clear()
57
+
58
+ #
59
+
60
+ class Diff(ta.NamedTuple):
61
+ added: ta.List[ProcessGroupConfig]
62
+ changed: ta.List[ProcessGroupConfig]
63
+ removed: ta.List[ProcessGroupConfig]
64
+
65
+ def diff(self, new: ta.Sequence[ProcessGroupConfig]) -> Diff:
66
+ cur = [group.config for group in self]
67
+
68
+ cur_by_name = {cfg.name: cfg for cfg in cur}
69
+ new_by_name = {cfg.name: cfg for cfg in new}
70
+
71
+ added = [cand for cand in new if cand.name not in cur_by_name]
72
+ removed = [cand for cand in cur if cand.name not in new_by_name]
73
+ changed = [cand for cand in new if cand != cur_by_name.get(cand.name, cand)]
74
+
75
+ return ProcessGroupManager.Diff(
76
+ added,
77
+ changed,
78
+ removed,
79
+ )
@@ -0,0 +1,86 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.check import check_isinstance
5
+ from omlish.lite.typing import Func2
6
+
7
+ from .configs import ProcessConfig
8
+ from .configs import ProcessGroupConfig
9
+ from .states import ProcessState
10
+ from .types import Process
11
+ from .types import ProcessGroup
12
+
13
+
14
+ class ProcessFactory(Func2[ProcessConfig, ProcessGroup, Process]):
15
+ pass
16
+
17
+
18
+ class ProcessGroupImpl(ProcessGroup):
19
+ def __init__(
20
+ self,
21
+ config: ProcessGroupConfig,
22
+ *,
23
+ process_factory: ProcessFactory,
24
+ ):
25
+ super().__init__()
26
+
27
+ self._config = config
28
+ self._process_factory = process_factory
29
+
30
+ by_name: ta.Dict[str, Process] = {}
31
+ for pconfig in self._config.processes or []:
32
+ p = check_isinstance(self._process_factory(pconfig, self), Process)
33
+ if p.name in by_name:
34
+ raise KeyError(f'name {p.name} of process {p} already registered by {by_name[p.name]}')
35
+ by_name[pconfig.name] = p
36
+ self._by_name = by_name
37
+
38
+ @property
39
+ def _by_key(self) -> ta.Mapping[str, Process]:
40
+ return self._by_name
41
+
42
+ #
43
+
44
+ def __repr__(self) -> str:
45
+ return f'<{self.__class__.__name__} instance at {id(self)} named {self._config.name}>'
46
+
47
+ #
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ return self._config.name
52
+
53
+ @property
54
+ def config(self) -> ProcessGroupConfig:
55
+ return self._config
56
+
57
+ @property
58
+ def by_name(self) -> ta.Mapping[str, Process]:
59
+ return self._by_name
60
+
61
+ #
62
+
63
+ def get_unstopped_processes(self) -> ta.List[Process]:
64
+ return [x for x in self if not x.get_state().stopped]
65
+
66
+ def stop_all(self) -> None:
67
+ processes = list(self._by_name.values())
68
+ processes.sort()
69
+ processes.reverse() # stop in desc priority order
70
+
71
+ for proc in processes:
72
+ state = proc.get_state()
73
+ if state == ProcessState.RUNNING:
74
+ # RUNNING -> STOPPING
75
+ proc.stop()
76
+
77
+ elif state == ProcessState.STARTING:
78
+ # STARTING -> STOPPING
79
+ proc.stop()
80
+
81
+ elif state == ProcessState.BACKOFF:
82
+ # BACKOFF -> FATAL
83
+ proc.give_up()
84
+
85
+ def before_remove(self) -> None:
86
+ pass
@@ -7,23 +7,33 @@ from omlish.lite.inject import inj
7
7
 
8
8
  from .configs import ServerConfig
9
9
  from .context import ServerContextImpl
10
- from .context import ServerEpoch
10
+ from .dispatchersimpl import InputDispatcherImpl
11
+ from .dispatchersimpl import OutputDispatcherImpl
11
12
  from .events import EventCallbacks
12
- from .groups import ProcessFactory
13
- from .groups import ProcessGroupImpl
13
+ from .groups import ProcessGroupManager
14
+ from .groupsimpl import ProcessFactory
15
+ from .groupsimpl import ProcessGroupImpl
14
16
  from .poller import Poller
15
17
  from .poller import get_poller_impl
16
- from .process import InheritedFds
17
- from .process import ProcessImpl
18
+ from .processes import PidHistory
19
+ from .processesimpl import ProcessImpl
20
+ from .processesimpl import ProcessSpawningFactory
21
+ from .setup import DaemonizeListener
22
+ from .setup import DaemonizeListeners
23
+ from .setup import SupervisorUser
24
+ from .setupimpl import SupervisorSetup
25
+ from .setupimpl import SupervisorSetupImpl
18
26
  from .signals import SignalReceiver
27
+ from .spawningimpl import InheritedFds
28
+ from .spawningimpl import InputDispatcherFactory
29
+ from .spawningimpl import OutputDispatcherFactory
30
+ from .spawningimpl import ProcessSpawningImpl
19
31
  from .supervisor import ProcessGroupFactory
20
- from .supervisor import ProcessGroups
21
32
  from .supervisor import SignalHandler
22
33
  from .supervisor import Supervisor
23
34
  from .types import ServerContext
24
-
25
-
26
- ##
35
+ from .types import ServerEpoch
36
+ from .users import get_user
27
37
 
28
38
 
29
39
  def bind_server(
@@ -35,7 +45,12 @@ def bind_server(
35
45
  lst: ta.List[InjectorBindingOrBindings] = [
36
46
  inj.bind(config),
37
47
 
38
- inj.bind(get_poller_impl(), key=Poller, singleton=True),
48
+ inj.bind_array_type(DaemonizeListener, DaemonizeListeners),
49
+
50
+ inj.bind(SupervisorSetupImpl, singleton=True),
51
+ inj.bind(SupervisorSetup, to_key=SupervisorSetupImpl),
52
+
53
+ inj.bind(DaemonizeListener, array=True, to_key=Poller),
39
54
 
40
55
  inj.bind(ServerContextImpl, singleton=True),
41
56
  inj.bind(ServerContext, to_key=ServerContextImpl),
@@ -45,11 +60,18 @@ def bind_server(
45
60
  inj.bind(SignalReceiver, singleton=True),
46
61
 
47
62
  inj.bind(SignalHandler, singleton=True),
48
- inj.bind(ProcessGroups, singleton=True),
63
+ inj.bind(ProcessGroupManager, singleton=True),
49
64
  inj.bind(Supervisor, singleton=True),
50
65
 
51
- inj.bind_factory(ProcessGroupFactory, ProcessGroupImpl),
52
- inj.bind_factory(ProcessFactory, ProcessImpl),
66
+ inj.bind(PidHistory()),
67
+
68
+ inj.bind_factory(ProcessGroupImpl, ProcessGroupFactory),
69
+ inj.bind_factory(ProcessImpl, ProcessFactory),
70
+
71
+ inj.bind_factory(ProcessSpawningImpl, ProcessSpawningFactory),
72
+
73
+ inj.bind_factory(OutputDispatcherImpl, OutputDispatcherFactory),
74
+ inj.bind_factory(InputDispatcherImpl, InputDispatcherFactory),
53
75
  ]
54
76
 
55
77
  #
@@ -61,4 +83,14 @@ def bind_server(
61
83
 
62
84
  #
63
85
 
86
+ if config.user is not None:
87
+ user = get_user(config.user)
88
+ lst.append(inj.bind(user, key=SupervisorUser))
89
+
90
+ #
91
+
92
+ lst.append(inj.bind(get_poller_impl(), key=Poller, singleton=True))
93
+
94
+ #
95
+
64
96
  return inj.as_bindings(*lst)
@@ -44,7 +44,7 @@ from .configs import prepare_server_config
44
44
  from .context import ServerContextImpl
45
45
  from .context import ServerEpoch
46
46
  from .inject import bind_server
47
- from .process import InheritedFds
47
+ from .spawningimpl import InheritedFds
48
48
  from .states import SupervisorState
49
49
  from .supervisor import Supervisor
50
50
  from .utils import ExitNow
@@ -0,0 +1,83 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import fcntl
4
+ import os
5
+ import typing as ta
6
+
7
+ from .utils import close_fd
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class ProcessPipes:
12
+ child_stdin: ta.Optional[int] = None
13
+ stdin: ta.Optional[int] = None
14
+
15
+ stdout: ta.Optional[int] = None
16
+ child_stdout: ta.Optional[int] = None
17
+
18
+ stderr: ta.Optional[int] = None
19
+ child_stderr: ta.Optional[int] = None
20
+
21
+ def child_fds(self) -> ta.List[int]:
22
+ return [fd for fd in [self.child_stdin, self.child_stdout, self.child_stderr] if fd is not None]
23
+
24
+ def parent_fds(self) -> ta.List[int]:
25
+ return [fd for fd in [self.stdin, self.stdout, self.stderr] if fd is not None]
26
+
27
+
28
+ def make_process_pipes(stderr=True) -> ProcessPipes:
29
+ """
30
+ Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
31
+ read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
32
+ """
33
+
34
+ pipes: ta.Dict[str, ta.Optional[int]] = {
35
+ 'child_stdin': None,
36
+ 'stdin': None,
37
+
38
+ 'stdout': None,
39
+ 'child_stdout': None,
40
+
41
+ 'stderr': None,
42
+ 'child_stderr': None,
43
+ }
44
+
45
+ try:
46
+ pipes['child_stdin'], pipes['stdin'] = os.pipe()
47
+ pipes['stdout'], pipes['child_stdout'] = os.pipe()
48
+
49
+ if stderr:
50
+ pipes['stderr'], pipes['child_stderr'] = os.pipe()
51
+
52
+ for fd in (
53
+ pipes['stdout'],
54
+ pipes['stderr'],
55
+ pipes['stdin'],
56
+ ):
57
+ if fd is not None:
58
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
59
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
60
+
61
+ return ProcessPipes(**pipes)
62
+
63
+ except OSError:
64
+ for fd in pipes.values():
65
+ if fd is not None:
66
+ close_fd(fd)
67
+
68
+ raise
69
+
70
+
71
+ def close_pipes(pipes: ProcessPipes) -> None:
72
+ close_parent_pipes(pipes)
73
+ close_child_pipes(pipes)
74
+
75
+
76
+ def close_parent_pipes(pipes: ProcessPipes) -> None:
77
+ for fd in pipes.parent_fds():
78
+ close_fd(fd)
79
+
80
+
81
+ def close_child_pipes(pipes: ProcessPipes) -> None:
82
+ for fd in pipes.child_fds():
83
+ close_fd(fd)
@@ -7,8 +7,10 @@ import typing as ta
7
7
 
8
8
  from omlish.lite.logs import log
9
9
 
10
+ from .setup import DaemonizeListener
10
11
 
11
- class Poller(abc.ABC):
12
+
13
+ class Poller(DaemonizeListener, abc.ABC):
12
14
  def __init__(self) -> None:
13
15
  super().__init__()
14
16
 
@@ -226,8 +228,9 @@ else:
226
228
 
227
229
  def get_poller_impl() -> ta.Type[Poller]:
228
230
  if (
229
- sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
230
- hasattr(select, 'kqueue') and KqueuePoller is not None
231
+ (sys.platform == 'darwin' or sys.platform.startswith('freebsd')) and
232
+ hasattr(select, 'kqueue') and
233
+ KqueuePoller is not None
231
234
  ):
232
235
  return KqueuePoller
233
236
  elif hasattr(select, 'poll'):
@@ -0,0 +1,65 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import grp
3
+ import os
4
+ import pwd
5
+ import typing as ta
6
+
7
+
8
+ def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
9
+ """
10
+ Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
11
+ and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
12
+ dropped.
13
+ """
14
+
15
+ if user is None:
16
+ return 'No user specified to setuid to!'
17
+
18
+ # get uid for user, which can be a number or username
19
+ try:
20
+ uid = int(user)
21
+ except ValueError:
22
+ try:
23
+ pwrec = pwd.getpwnam(user) # type: ignore
24
+ except KeyError:
25
+ return f"Can't find username {user!r}"
26
+ uid = pwrec[2]
27
+ else:
28
+ try:
29
+ pwrec = pwd.getpwuid(uid)
30
+ except KeyError:
31
+ return f"Can't find uid {uid!r}"
32
+
33
+ current_uid = os.getuid()
34
+
35
+ if current_uid == uid:
36
+ # do nothing and return successfully if the uid is already the current one. this allows a supervisord
37
+ # running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
38
+ # it.
39
+ return None
40
+
41
+ if current_uid != 0:
42
+ return "Can't drop privilege as nonroot user"
43
+
44
+ gid = pwrec[3]
45
+ if hasattr(os, 'setgroups'):
46
+ user = pwrec[0]
47
+ groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
48
+
49
+ # always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
50
+ # group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
51
+ # python 2.7 - this will be safe though for all unix /python version combos)
52
+ groups.insert(0, gid)
53
+ try:
54
+ os.setgroups(groups)
55
+ except OSError:
56
+ return 'Could not set groups of effective user'
57
+
58
+ try:
59
+ os.setgid(gid)
60
+ except OSError:
61
+ return 'Could not set group id of effective user'
62
+
63
+ os.setuid(uid)
64
+
65
+ return None
@@ -0,0 +1,18 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from .types import Process
5
+
6
+
7
+ ##
8
+
9
+
10
+ class ProcessStateError(RuntimeError):
11
+ pass
12
+
13
+
14
+ ##
15
+
16
+
17
+ class PidHistory(ta.Dict[int, Process]):
18
+ pass