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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. ominfra/deploy/_executor.py +24 -0
  2. ominfra/pyremote/_runcommands.py +24 -0
  3. ominfra/scripts/journald2aws.py +24 -0
  4. ominfra/scripts/supervisor.py +1320 -1225
  5. ominfra/supervisor/configs.py +34 -11
  6. ominfra/supervisor/dispatchers.py +7 -6
  7. ominfra/supervisor/dispatchersimpl.py +29 -22
  8. ominfra/supervisor/groups.py +1 -1
  9. ominfra/supervisor/groupsimpl.py +2 -2
  10. ominfra/supervisor/inject.py +22 -17
  11. ominfra/supervisor/io.py +82 -0
  12. ominfra/supervisor/main.py +6 -7
  13. ominfra/supervisor/pipes.py +15 -13
  14. ominfra/supervisor/poller.py +36 -35
  15. ominfra/supervisor/{processes.py → process.py} +2 -1
  16. ominfra/supervisor/{processesimpl.py → processimpl.py} +42 -54
  17. ominfra/supervisor/setup.py +1 -1
  18. ominfra/supervisor/setupimpl.py +4 -3
  19. ominfra/supervisor/signals.py +56 -50
  20. ominfra/supervisor/spawning.py +2 -1
  21. ominfra/supervisor/spawningimpl.py +24 -21
  22. ominfra/supervisor/supervisor.py +72 -134
  23. ominfra/supervisor/types.py +45 -34
  24. ominfra/supervisor/utils/__init__.py +0 -0
  25. ominfra/supervisor/utils/diag.py +31 -0
  26. ominfra/supervisor/utils/fds.py +46 -0
  27. ominfra/supervisor/utils/fs.py +47 -0
  28. ominfra/supervisor/utils/os.py +45 -0
  29. ominfra/supervisor/utils/ostypes.py +9 -0
  30. ominfra/supervisor/utils/signals.py +60 -0
  31. ominfra/supervisor/utils/strings.py +105 -0
  32. ominfra/supervisor/{users.py → utils/users.py} +11 -8
  33. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/METADATA +3 -3
  34. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/RECORD +39 -33
  35. ominfra/supervisor/context.py +0 -84
  36. ominfra/supervisor/datatypes.py +0 -113
  37. ominfra/supervisor/utils.py +0 -206
  38. /ominfra/supervisor/{collections.py → utils/collections.py} +0 -0
  39. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/LICENSE +0 -0
  40. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/WHEEL +0 -0
  41. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/entry_points.txt +0 -0
  42. {ominfra-0.0.0.dev127.dist-info → ominfra-0.0.0.dev129.dist-info}/top_level.txt +0 -0
@@ -5,11 +5,12 @@ import dataclasses as dc
5
5
  from .dispatchers import Dispatchers
6
6
  from .pipes import ProcessPipes
7
7
  from .types import Process
8
+ from .utils.ostypes import Pid
8
9
 
9
10
 
10
11
  @dc.dataclass(frozen=True)
11
12
  class SpawnedProcess:
12
- pid: int
13
+ pid: Pid
13
14
  pipes: ProcessPipes
14
15
  dispatchers: Dispatchers
15
16
 
@@ -25,31 +25,34 @@ from .pipes import close_child_pipes
25
25
  from .pipes import close_pipes
26
26
  from .pipes import make_process_pipes
27
27
  from .privileges import drop_privileges
28
- from .processes import PidHistory
28
+ from .process import PidHistory
29
29
  from .spawning import ProcessSpawnError
30
30
  from .spawning import ProcessSpawning
31
31
  from .spawning import SpawnedProcess
32
32
  from .types import Dispatcher
33
- from .types import InputDispatcher
34
- from .types import OutputDispatcher
35
33
  from .types import Process
36
34
  from .types import ProcessGroup
37
- from .utils import as_bytes
38
- from .utils import close_fd
39
- from .utils import compact_traceback
40
- from .utils import get_path
41
- from .utils import real_exit
42
-
43
-
44
- class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], int, OutputDispatcher]):
35
+ from .types import ProcessInputDispatcher
36
+ from .types import ProcessOutputDispatcher
37
+ from .utils.diag import compact_traceback
38
+ from .utils.fds import close_fd
39
+ from .utils.fs import get_path
40
+ from .utils.os import real_exit
41
+ from .utils.ostypes import Fd
42
+ from .utils.ostypes import Pid
43
+ from .utils.ostypes import Rc
44
+ from .utils.strings import as_bytes
45
+
46
+
47
+ class ProcessOutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, ProcessOutputDispatcher]):
45
48
  pass
46
49
 
47
50
 
48
- class InputDispatcherFactory(Func3[Process, str, int, InputDispatcher]):
51
+ class ProcessInputDispatcherFactory(Func3[Process, str, Fd, ProcessInputDispatcher]):
49
52
  pass
50
53
 
51
54
 
52
- InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
55
+ InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[Fd])
53
56
 
54
57
 
55
58
  ##
@@ -63,8 +66,8 @@ class ProcessSpawningImpl(ProcessSpawning):
63
66
  server_config: ServerConfig,
64
67
  pid_history: PidHistory,
65
68
 
66
- output_dispatcher_factory: OutputDispatcherFactory,
67
- input_dispatcher_factory: InputDispatcherFactory,
69
+ output_dispatcher_factory: ProcessOutputDispatcherFactory,
70
+ input_dispatcher_factory: ProcessInputDispatcherFactory,
68
71
 
69
72
  inherited_fds: ta.Optional[InheritedFds] = None,
70
73
  ) -> None:
@@ -120,7 +123,7 @@ class ProcessSpawningImpl(ProcessSpawning):
120
123
  raise ProcessSpawnError(f"Unknown error making dispatchers for '{self.process.name}': {exc}") from exc
121
124
 
122
125
  try:
123
- pid = os.fork()
126
+ pid = Pid(os.fork())
124
127
  except OSError as exc:
125
128
  code = exc.args[0]
126
129
  if code == errno.EAGAIN:
@@ -204,21 +207,21 @@ class ProcessSpawningImpl(ProcessSpawning):
204
207
  self.process,
205
208
  ProcessCommunicationStdoutEvent,
206
209
  pipes.stdout,
207
- ), OutputDispatcher))
210
+ ), ProcessOutputDispatcher))
208
211
 
209
212
  if pipes.stderr is not None:
210
213
  dispatchers.append(check_isinstance(self._output_dispatcher_factory(
211
214
  self.process,
212
215
  ProcessCommunicationStderrEvent,
213
216
  pipes.stderr,
214
- ), OutputDispatcher))
217
+ ), ProcessOutputDispatcher))
215
218
 
216
219
  if pipes.stdin is not None:
217
220
  dispatchers.append(check_isinstance(self._input_dispatcher_factory(
218
221
  self.process,
219
222
  'stdin',
220
223
  pipes.stdin,
221
- ), InputDispatcher))
224
+ ), ProcessInputDispatcher))
222
225
 
223
226
  return Dispatchers(dispatchers)
224
227
 
@@ -299,7 +302,7 @@ class ProcessSpawningImpl(ProcessSpawning):
299
302
 
300
303
  finally:
301
304
  os.write(2, as_bytes('supervisor: child process was not spawned\n'))
302
- real_exit(127) # exit process with code for spawn failure
305
+ real_exit(Rc(127)) # exit process with code for spawn failure
303
306
 
304
307
  raise RuntimeError('Unreachable')
305
308
 
@@ -316,7 +319,7 @@ class ProcessSpawningImpl(ProcessSpawning):
316
319
  for i in range(3, self._server_config.minfds):
317
320
  if i in self._inherited_fds:
318
321
  continue
319
- close_fd(i)
322
+ close_fd(Fd(i))
320
323
 
321
324
  def _set_uid(self) -> ta.Optional[str]:
322
325
  if self.config.uid is None:
@@ -1,92 +1,57 @@
1
1
  # ruff: noqa: UP006 UP007
2
- import signal
2
+ import errno
3
+ import os
3
4
  import time
4
5
  import typing as ta
5
6
 
6
7
  from omlish.lite.check import check_isinstance
7
- from omlish.lite.check import check_not_none
8
8
  from omlish.lite.logs import log
9
9
  from omlish.lite.typing import Func1
10
10
 
11
11
  from .configs import ProcessGroupConfig
12
- from .context import ServerContextImpl
13
- from .dispatchers import Dispatchers
12
+ from .configs import ServerConfig
14
13
  from .events import TICK_EVENTS
15
14
  from .events import EventCallbacks
16
15
  from .events import SupervisorRunningEvent
17
16
  from .events import SupervisorStoppingEvent
18
17
  from .groups import ProcessGroup
19
18
  from .groups import ProcessGroupManager
19
+ from .io import IoManager
20
20
  from .poller import Poller
21
- from .processes import PidHistory
21
+ from .process import PidHistory
22
22
  from .setup import SupervisorSetup
23
- from .signals import SignalReceiver
24
- from .signals import sig_name
23
+ from .signals import SignalHandler
25
24
  from .states import SupervisorState
26
- from .types import OutputDispatcher
25
+ from .types import ExitNow
27
26
  from .types import Process
28
- from .utils import ExitNow
29
- from .utils import as_string
30
- from .utils import decode_wait_status
31
- from .utils import timeslice
27
+ from .types import SupervisorStateManager
28
+ from .utils.os import decode_wait_status
29
+ from .utils.ostypes import Pid
30
+ from .utils.ostypes import Rc
32
31
 
33
32
 
34
33
  ##
35
34
 
36
35
 
37
- class SignalHandler:
38
- def __init__(
39
- self,
40
- *,
41
- context: ServerContextImpl,
42
- signal_receiver: SignalReceiver,
43
- process_groups: ProcessGroupManager,
44
- ) -> None:
45
- super().__init__()
36
+ def timeslice(period: int, when: float) -> int:
37
+ return int(when - (when % period))
46
38
 
47
- self._context = context
48
- self._signal_receiver = signal_receiver
49
- self._process_groups = process_groups
50
-
51
- def set_signals(self) -> None:
52
- self._signal_receiver.install(
53
- signal.SIGTERM,
54
- signal.SIGINT,
55
- signal.SIGQUIT,
56
- signal.SIGHUP,
57
- signal.SIGCHLD,
58
- signal.SIGUSR2,
59
- )
60
-
61
- def handle_signals(self) -> None:
62
- sig = self._signal_receiver.get_signal()
63
- if not sig:
64
- return
65
39
 
66
- if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
67
- log.warning('received %s indicating exit request', sig_name(sig))
68
- self._context.set_state(SupervisorState.SHUTDOWN)
40
+ ##
69
41
 
70
- elif sig == signal.SIGHUP:
71
- if self._context.state == SupervisorState.SHUTDOWN:
72
- log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
73
- else:
74
- log.warning('received %s indicating restart request', sig_name(sig)) # noqa
75
- self._context.set_state(SupervisorState.RESTARTING)
76
42
 
77
- elif sig == signal.SIGCHLD:
78
- log.debug('received %s indicating a child quit', sig_name(sig))
43
+ class SupervisorStateManagerImpl(SupervisorStateManager):
44
+ def __init__(self) -> None:
45
+ super().__init__()
79
46
 
80
- elif sig == signal.SIGUSR2:
81
- log.info('received %s indicating log reopen request', sig_name(sig))
47
+ self._state: SupervisorState = SupervisorState.RUNNING
82
48
 
83
- for p in self._process_groups.all_processes():
84
- for d in p.get_dispatchers():
85
- if isinstance(d, OutputDispatcher):
86
- d.reopen_logs()
49
+ @property
50
+ def state(self) -> SupervisorState:
51
+ return self._state
87
52
 
88
- else:
89
- log.debug('received %s indicating nothing', sig_name(sig))
53
+ def set_state(self, state: SupervisorState) -> None:
54
+ self._state = state
90
55
 
91
56
 
92
57
  ##
@@ -100,7 +65,7 @@ class Supervisor:
100
65
  def __init__(
101
66
  self,
102
67
  *,
103
- context: ServerContextImpl,
68
+ config: ServerConfig,
104
69
  poller: Poller,
105
70
  process_groups: ProcessGroupManager,
106
71
  signal_handler: SignalHandler,
@@ -108,10 +73,12 @@ class Supervisor:
108
73
  process_group_factory: ProcessGroupFactory,
109
74
  pid_history: PidHistory,
110
75
  setup: SupervisorSetup,
76
+ states: SupervisorStateManager,
77
+ io: IoManager,
111
78
  ) -> None:
112
79
  super().__init__()
113
80
 
114
- self._context = context
81
+ self._config = config
115
82
  self._poller = poller
116
83
  self._process_groups = process_groups
117
84
  self._signal_handler = signal_handler
@@ -119,6 +86,8 @@ class Supervisor:
119
86
  self._process_group_factory = process_group_factory
120
87
  self._pid_history = pid_history
121
88
  self._setup = setup
89
+ self._states = states
90
+ self._io = io
122
91
 
123
92
  self._ticks: ta.Dict[int, float] = {}
124
93
  self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
@@ -128,11 +97,8 @@ class Supervisor:
128
97
  #
129
98
 
130
99
  @property
131
- def context(self) -> ServerContextImpl:
132
- return self._context
133
-
134
- def get_state(self) -> SupervisorState:
135
- return self._context.state
100
+ def state(self) -> SupervisorState:
101
+ return self._states.state
136
102
 
137
103
  #
138
104
 
@@ -168,12 +134,12 @@ class Supervisor:
168
134
  # throttle 'waiting for x to die' reports
169
135
  now = time.time()
170
136
  if now > (self._last_shutdown_report + 3): # every 3 secs
171
- names = [as_string(p.config.name) for p in unstopped]
137
+ names = [p.config.name for p in unstopped]
172
138
  namestr = ', '.join(names)
173
139
  log.info('waiting for %s to die', namestr)
174
140
  self._last_shutdown_report = now
175
141
  for proc in unstopped:
176
- log.debug('%s state: %s', proc.config.name, proc.get_state().name)
142
+ log.debug('%s state: %s', proc.config.name, proc.state.name)
177
143
 
178
144
  return unstopped
179
145
 
@@ -197,7 +163,7 @@ class Supervisor:
197
163
  self._event_callbacks.clear()
198
164
 
199
165
  try:
200
- for config in self._context.config.groups or []:
166
+ for config in self._config.groups or []:
201
167
  self.add_process_group(config)
202
168
 
203
169
  self._signal_handler.set_signals()
@@ -221,7 +187,7 @@ class Supervisor:
221
187
  self._signal_handler.handle_signals()
222
188
  self._tick()
223
189
 
224
- if self._context.state < SupervisorState.RUNNING:
190
+ if self._states.state < SupervisorState.RUNNING:
225
191
  self._ordered_stop_groups_phase_2()
226
192
 
227
193
  def _ordered_stop_groups_phase_1(self) -> None:
@@ -240,20 +206,11 @@ class Supervisor:
240
206
  # down, so push it back on to the end of the stop group queue
241
207
  self._stop_groups.append(group)
242
208
 
243
- def get_dispatchers(self) -> Dispatchers:
244
- return Dispatchers(
245
- d
246
- for p in self._process_groups.all_processes()
247
- for d in p.get_dispatchers()
248
- )
249
-
250
209
  def _poll(self) -> None:
251
- dispatchers = self.get_dispatchers()
252
-
253
210
  sorted_groups = list(self._process_groups)
254
211
  sorted_groups.sort()
255
212
 
256
- if self._context.state < SupervisorState.RUNNING:
213
+ if self._states.state < SupervisorState.RUNNING:
257
214
  if not self._stopping:
258
215
  # first time, set the stopping flag, do a notification and set stop_groups
259
216
  self._stopping = True
@@ -266,54 +223,7 @@ class Supervisor:
266
223
  # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
267
224
  raise ExitNow
268
225
 
269
- for fd, dispatcher in dispatchers.items():
270
- if dispatcher.readable():
271
- self._poller.register_readable(fd)
272
- if dispatcher.writable():
273
- self._poller.register_writable(fd)
274
-
275
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
276
- r, w = self._poller.poll(timeout)
277
-
278
- for fd in r:
279
- if fd in dispatchers:
280
- try:
281
- dispatcher = dispatchers[fd]
282
- log.debug('read event caused by %r', dispatcher)
283
- dispatcher.handle_read_event()
284
- if not dispatcher.readable():
285
- self._poller.unregister_readable(fd)
286
- except ExitNow:
287
- raise
288
- except Exception: # noqa
289
- dispatchers[fd].handle_error()
290
- else:
291
- # if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
292
- # time, which may cause 100% cpu usage
293
- log.debug('unexpected read event from fd %r', fd)
294
- try:
295
- self._poller.unregister_readable(fd)
296
- except Exception: # noqa
297
- pass
298
-
299
- for fd in w:
300
- if fd in dispatchers:
301
- try:
302
- dispatcher = dispatchers[fd]
303
- log.debug('write event caused by %r', dispatcher)
304
- dispatcher.handle_write_event()
305
- if not dispatcher.writable():
306
- self._poller.unregister_writable(fd)
307
- except ExitNow:
308
- raise
309
- except Exception: # noqa
310
- dispatchers[fd].handle_error()
311
- else:
312
- log.debug('unexpected write event from fd %r', fd)
313
- try:
314
- self._poller.unregister_writable(fd)
315
- except Exception: # noqa
316
- pass
226
+ self._io.poll()
317
227
 
318
228
  for group in sorted_groups:
319
229
  for process in group:
@@ -323,17 +233,17 @@ class Supervisor:
323
233
  if depth >= 100:
324
234
  return
325
235
 
326
- pid, sts = self._context.waitpid()
327
- if not pid:
236
+ wp = waitpid()
237
+ if wp is None or not wp.pid:
328
238
  return
329
239
 
330
- process = self._pid_history.get(pid, None)
240
+ process = self._pid_history.get(wp.pid, None)
331
241
  if process is None:
332
- _, msg = decode_wait_status(check_not_none(sts))
333
- log.info('reaped unknown pid %s (%s)', pid, msg)
242
+ _, msg = decode_wait_status(wp.sts)
243
+ log.info('reaped unknown pid %s (%s)', wp.pid, msg)
334
244
  else:
335
- process.finish(check_not_none(sts))
336
- del self._pid_history[pid]
245
+ process.finish(wp.sts)
246
+ del self._pid_history[wp.pid]
337
247
 
338
248
  if not once:
339
249
  # keep reaping until no more kids to reap, but don't recurse infinitely
@@ -358,3 +268,31 @@ class Supervisor:
358
268
  if this_tick != last_tick:
359
269
  self._ticks[period] = this_tick
360
270
  self._event_callbacks.notify(event(this_tick, self))
271
+
272
+
273
+ ##
274
+
275
+
276
+ class WaitedPid(ta.NamedTuple):
277
+ pid: Pid
278
+ sts: Rc
279
+
280
+
281
+ def waitpid() -> ta.Optional[WaitedPid]:
282
+ # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
283
+ # still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
284
+ # waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
285
+ # normal course in the mainloop), we'll eventually reap the child that we tried to reap during the interrupted
286
+ # call. At least on Linux, this appears to be true, or at least stopping 50 processes at once never left zombies
287
+ # lying around.
288
+ try:
289
+ pid, sts = os.waitpid(-1, os.WNOHANG)
290
+ except OSError as exc:
291
+ code = exc.args[0]
292
+ if code not in (errno.ECHILD, errno.EINTR):
293
+ log.critical('waitpid error %r; a process may not be cleaned up properly', code)
294
+ if code == errno.EINTR:
295
+ log.debug('EINTR during reap')
296
+ return None
297
+ else:
298
+ return WaitedPid(pid, sts) # type: ignore
@@ -3,12 +3,14 @@ import abc
3
3
  import functools
4
4
  import typing as ta
5
5
 
6
- from .collections import KeyedCollectionAccessors
7
6
  from .configs import ProcessConfig
8
7
  from .configs import ProcessGroupConfig
9
- from .configs import ServerConfig
10
8
  from .states import ProcessState
11
9
  from .states import SupervisorState
10
+ from .utils.collections import KeyedCollectionAccessors
11
+ from .utils.ostypes import Fd
12
+ from .utils.ostypes import Pid
13
+ from .utils.ostypes import Rc
12
14
 
13
15
 
14
16
  if ta.TYPE_CHECKING:
@@ -18,6 +20,10 @@ if ta.TYPE_CHECKING:
18
20
  ##
19
21
 
20
22
 
23
+ class ExitNow(Exception): # noqa
24
+ pass
25
+
26
+
21
27
  ServerEpoch = ta.NewType('ServerEpoch', int)
22
28
 
23
29
 
@@ -41,12 +47,7 @@ class ConfigPriorityOrdered(abc.ABC):
41
47
  ##
42
48
 
43
49
 
44
- class ServerContext(abc.ABC):
45
- @property
46
- @abc.abstractmethod
47
- def config(self) -> ServerConfig:
48
- raise NotImplementedError
49
-
50
+ class SupervisorStateManager(abc.ABC):
50
51
  @property
51
52
  @abc.abstractmethod
52
53
  def state(self) -> SupervisorState:
@@ -56,21 +57,11 @@ class ServerContext(abc.ABC):
56
57
  def set_state(self, state: SupervisorState) -> None:
57
58
  raise NotImplementedError
58
59
 
59
- @property
60
- @abc.abstractmethod
61
- def pid_history(self) -> ta.Dict[int, 'Process']:
62
- raise NotImplementedError
63
-
64
60
 
65
61
  ##
66
62
 
67
63
 
68
64
  class Dispatcher(abc.ABC):
69
- @property
70
- @abc.abstractmethod
71
- def process(self) -> 'Process':
72
- raise NotImplementedError
73
-
74
65
  @property
75
66
  @abc.abstractmethod
76
67
  def channel(self) -> str:
@@ -78,7 +69,7 @@ class Dispatcher(abc.ABC):
78
69
 
79
70
  @property
80
71
  @abc.abstractmethod
81
- def fd(self) -> int:
72
+ def fd(self) -> Fd:
82
73
  raise NotImplementedError
83
74
 
84
75
  @property
@@ -114,8 +105,32 @@ class Dispatcher(abc.ABC):
114
105
  def handle_write_event(self) -> None:
115
106
  raise TypeError
116
107
 
108
+ #
109
+
110
+ def handle_connect(self) -> None:
111
+ raise TypeError
112
+
113
+ def handle_close(self) -> None:
114
+ raise TypeError
115
+
116
+ def handle_accepted(self, sock, addr) -> None:
117
+ raise TypeError
118
+
117
119
 
118
- class OutputDispatcher(Dispatcher, abc.ABC):
120
+ class HasDispatchers(abc.ABC):
121
+ @abc.abstractmethod
122
+ def get_dispatchers(self) -> 'Dispatchers':
123
+ raise NotImplementedError
124
+
125
+
126
+ class ProcessDispatcher(Dispatcher, abc.ABC):
127
+ @property
128
+ @abc.abstractmethod
129
+ def process(self) -> 'Process':
130
+ raise NotImplementedError
131
+
132
+
133
+ class ProcessOutputDispatcher(ProcessDispatcher, abc.ABC):
119
134
  @abc.abstractmethod
120
135
  def remove_logs(self) -> None:
121
136
  raise NotImplementedError
@@ -125,7 +140,7 @@ class OutputDispatcher(Dispatcher, abc.ABC):
125
140
  raise NotImplementedError
126
141
 
127
142
 
128
- class InputDispatcher(Dispatcher, abc.ABC):
143
+ class ProcessInputDispatcher(ProcessDispatcher, abc.ABC):
129
144
  @abc.abstractmethod
130
145
  def write(self, chars: ta.Union[bytes, str]) -> None:
131
146
  raise NotImplementedError
@@ -138,7 +153,11 @@ class InputDispatcher(Dispatcher, abc.ABC):
138
153
  ##
139
154
 
140
155
 
141
- class Process(ConfigPriorityOrdered, abc.ABC):
156
+ class Process(
157
+ ConfigPriorityOrdered,
158
+ HasDispatchers,
159
+ abc.ABC,
160
+ ):
142
161
  @property
143
162
  @abc.abstractmethod
144
163
  def name(self) -> str:
@@ -156,18 +175,13 @@ class Process(ConfigPriorityOrdered, abc.ABC):
156
175
 
157
176
  @property
158
177
  @abc.abstractmethod
159
- def pid(self) -> int:
178
+ def pid(self) -> Pid:
160
179
  raise NotImplementedError
161
180
 
162
181
  #
163
182
 
164
- @property
165
- @abc.abstractmethod
166
- def context(self) -> ServerContext:
167
- raise NotImplementedError
168
-
169
183
  @abc.abstractmethod
170
- def finish(self, sts: int) -> None:
184
+ def finish(self, sts: Rc) -> None:
171
185
  raise NotImplementedError
172
186
 
173
187
  @abc.abstractmethod
@@ -182,18 +196,15 @@ class Process(ConfigPriorityOrdered, abc.ABC):
182
196
  def transition(self) -> None:
183
197
  raise NotImplementedError
184
198
 
199
+ @property
185
200
  @abc.abstractmethod
186
- def get_state(self) -> ProcessState:
201
+ def state(self) -> ProcessState:
187
202
  raise NotImplementedError
188
203
 
189
204
  @abc.abstractmethod
190
205
  def after_setuid(self) -> None:
191
206
  raise NotImplementedError
192
207
 
193
- @abc.abstractmethod
194
- def get_dispatchers(self) -> 'Dispatchers':
195
- raise NotImplementedError
196
-
197
208
 
198
209
  ##
199
210
 
File without changes
@@ -0,0 +1,31 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import sys
3
+ import types
4
+ import typing as ta
5
+
6
+
7
+ def compact_traceback() -> ta.Tuple[
8
+ ta.Tuple[str, str, int],
9
+ ta.Type[BaseException],
10
+ BaseException,
11
+ types.TracebackType,
12
+ ]:
13
+ t, v, tb = sys.exc_info()
14
+ if not tb:
15
+ raise RuntimeError('No traceback')
16
+
17
+ tbinfo = []
18
+ while tb:
19
+ tbinfo.append((
20
+ tb.tb_frame.f_code.co_filename,
21
+ tb.tb_frame.f_code.co_name,
22
+ str(tb.tb_lineno),
23
+ ))
24
+ tb = tb.tb_next
25
+
26
+ # just to be safe
27
+ del tb
28
+
29
+ file, function, line = tbinfo[-1]
30
+ info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) # noqa
31
+ return (file, function, line), t, v, info # type: ignore
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import errno
3
+ import os
4
+ import typing as ta
5
+
6
+ from .ostypes import Fd
7
+
8
+
9
+ class PipeFds(ta.NamedTuple):
10
+ r: Fd
11
+ w: Fd
12
+
13
+
14
+ def make_pipe() -> PipeFds:
15
+ return PipeFds(*os.pipe()) # type: ignore
16
+
17
+
18
+ def read_fd(fd: Fd) -> bytes:
19
+ try:
20
+ data = os.read(fd, 2 << 16) # 128K
21
+ except OSError as why:
22
+ if why.args[0] not in (errno.EWOULDBLOCK, errno.EBADF, errno.EINTR):
23
+ raise
24
+ data = b''
25
+ return data
26
+
27
+
28
+ def close_fd(fd: Fd) -> bool:
29
+ try:
30
+ os.close(fd)
31
+ except OSError:
32
+ return False
33
+ return True
34
+
35
+
36
+ def is_fd_open(fd: Fd) -> bool:
37
+ try:
38
+ n = os.dup(fd)
39
+ except OSError:
40
+ return False
41
+ os.close(n)
42
+ return True
43
+
44
+
45
+ def get_open_fds(limit: int) -> ta.FrozenSet[Fd]:
46
+ return frozenset(fd for i in range(limit) if is_fd_open(fd := Fd(i)))