ominfra 0.0.0.dev131__py3-none-any.whl → 0.0.0.dev132__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1627,6 +1627,14 @@ class ExitStacked:
1627
1627
  ##
1628
1628
 
1629
1629
 
1630
+ @contextlib.contextmanager
1631
+ def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
1632
+ try:
1633
+ yield fn
1634
+ finally:
1635
+ fn()
1636
+
1637
+
1630
1638
  @contextlib.contextmanager
1631
1639
  def attr_setting(obj, attr, val, *, default=None): # noqa
1632
1640
  not_set = object()
@@ -116,6 +116,10 @@ A2 = ta.TypeVar('A2')
116
116
 
117
117
  # events.py
118
118
  EventCallback = ta.Callable[['Event'], None]
119
+ ProcessOutputChannel = ta.Literal['stdout', 'stderr'] # ta.TypeAlias
120
+
121
+ # ../../omlish/lite/contextmanagers.py
122
+ ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
119
123
 
120
124
  # ../../omlish/lite/http/parsing.py
121
125
  HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
@@ -1077,6 +1081,17 @@ class ProcessState(enum.IntEnum):
1077
1081
  return self in SIGNALABLE_STATES
1078
1082
 
1079
1083
 
1084
+ # http://supervisord.org/subprocess.html
1085
+ STATE_TRANSITIONS = {
1086
+ ProcessState.STOPPED: (ProcessState.STARTING,),
1087
+ ProcessState.STARTING: (ProcessState.RUNNING, ProcessState.BACKOFF, ProcessState.STOPPING),
1088
+ ProcessState.RUNNING: (ProcessState.STOPPING, ProcessState.EXITED),
1089
+ ProcessState.BACKOFF: (ProcessState.STARTING, ProcessState.FATAL),
1090
+ ProcessState.STOPPING: (ProcessState.STOPPED,),
1091
+ ProcessState.EXITED: (ProcessState.STARTING,),
1092
+ ProcessState.FATAL: (ProcessState.STARTING,),
1093
+ }
1094
+
1080
1095
  STOPPED_STATES = (
1081
1096
  ProcessState.STOPPED,
1082
1097
  ProcessState.EXITED,
@@ -1576,16 +1591,16 @@ class FdIoPoller(abc.ABC):
1576
1591
  def register_readable(self, fd: int) -> bool:
1577
1592
  if fd in self._readable:
1578
1593
  return False
1579
- self._readable.add(fd)
1580
1594
  self._register_readable(fd)
1595
+ self._readable.add(fd)
1581
1596
  return True
1582
1597
 
1583
1598
  @ta.final
1584
1599
  def register_writable(self, fd: int) -> bool:
1585
1600
  if fd in self._writable:
1586
1601
  return False
1587
- self._writable.add(fd)
1588
1602
  self._register_writable(fd)
1603
+ self._writable.add(fd)
1589
1604
  return True
1590
1605
 
1591
1606
  @ta.final
@@ -1689,24 +1704,24 @@ if hasattr(select, 'poll'):
1689
1704
 
1690
1705
  #
1691
1706
 
1692
- _READ = select.POLLIN | select.POLLPRI | select.POLLHUP
1693
- _WRITE = select.POLLOUT
1694
-
1695
1707
  def _register_readable(self, fd: int) -> None:
1696
- self._update_registration(fd)
1708
+ self._update_registration(fd, r=True, w=fd in self._writable)
1697
1709
 
1698
1710
  def _register_writable(self, fd: int) -> None:
1699
- self._update_registration(fd)
1711
+ self._update_registration(fd, r=fd in self._readable, w=True)
1700
1712
 
1701
1713
  def _unregister_readable(self, fd: int) -> None:
1702
- self._update_registration(fd)
1714
+ self._update_registration(fd, r=False, w=False)
1703
1715
 
1704
1716
  def _unregister_writable(self, fd: int) -> None:
1705
- self._update_registration(fd)
1717
+ self._update_registration(fd, r=fd in self._readable, w=False)
1706
1718
 
1707
- def _update_registration(self, fd: int) -> None:
1708
- r = fd in self._readable
1709
- w = fd in self._writable
1719
+ #
1720
+
1721
+ _READ = select.POLLIN | select.POLLPRI | select.POLLHUP
1722
+ _WRITE = select.POLLOUT
1723
+
1724
+ def _update_registration(self, fd: int, *, r: bool, w: bool) -> None:
1710
1725
  if r or w:
1711
1726
  self._poller.register(fd, (self._READ if r else 0) | (self._WRITE if w else 0))
1712
1727
  else:
@@ -2119,7 +2134,7 @@ class EventCallbacks:
2119
2134
 
2120
2135
 
2121
2136
  class ProcessLogEvent(Event, abc.ABC):
2122
- channel: ta.Optional[str] = None
2137
+ channel: ta.ClassVar[ProcessOutputChannel]
2123
2138
 
2124
2139
  def __init__(self, process, pid, data):
2125
2140
  super().__init__()
@@ -2144,7 +2159,7 @@ class ProcessCommunicationEvent(Event, abc.ABC):
2144
2159
  BEGIN_TOKEN = b'<!--XSUPERVISOR:BEGIN-->'
2145
2160
  END_TOKEN = b'<!--XSUPERVISOR:END-->'
2146
2161
 
2147
- channel: ta.ClassVar[str]
2162
+ channel: ta.ClassVar[ProcessOutputChannel]
2148
2163
 
2149
2164
  def __init__(self, process, pid, data):
2150
2165
  super().__init__()
@@ -2516,6 +2531,64 @@ def get_user(name: str) -> User:
2516
2531
  )
2517
2532
 
2518
2533
 
2534
+ ########################################
2535
+ # ../../../omlish/lite/contextmanagers.py
2536
+
2537
+
2538
+ ##
2539
+
2540
+
2541
+ class ExitStacked:
2542
+ _exit_stack: ta.Optional[contextlib.ExitStack] = None
2543
+
2544
+ def __enter__(self: ExitStackedT) -> ExitStackedT:
2545
+ check_state(self._exit_stack is None)
2546
+ es = self._exit_stack = contextlib.ExitStack()
2547
+ es.__enter__()
2548
+ return self
2549
+
2550
+ def __exit__(self, exc_type, exc_val, exc_tb):
2551
+ if (es := self._exit_stack) is None:
2552
+ return None
2553
+ self._exit_contexts()
2554
+ return es.__exit__(exc_type, exc_val, exc_tb)
2555
+
2556
+ def _exit_contexts(self) -> None:
2557
+ pass
2558
+
2559
+ def _enter_context(self, cm: ta.ContextManager[T]) -> T:
2560
+ es = check_not_none(self._exit_stack)
2561
+ return es.enter_context(cm)
2562
+
2563
+
2564
+ ##
2565
+
2566
+
2567
+ @contextlib.contextmanager
2568
+ def defer(fn: ta.Callable) -> ta.Generator[ta.Callable, None, None]:
2569
+ try:
2570
+ yield fn
2571
+ finally:
2572
+ fn()
2573
+
2574
+
2575
+ @contextlib.contextmanager
2576
+ def attr_setting(obj, attr, val, *, default=None): # noqa
2577
+ not_set = object()
2578
+ orig = getattr(obj, attr, not_set)
2579
+ try:
2580
+ setattr(obj, attr, val)
2581
+ if orig is not not_set:
2582
+ yield orig
2583
+ else:
2584
+ yield default
2585
+ finally:
2586
+ if orig is not_set:
2587
+ delattr(obj, attr)
2588
+ else:
2589
+ setattr(obj, attr, orig)
2590
+
2591
+
2519
2592
  ########################################
2520
2593
  # ../../../omlish/lite/fdio/handlers.py
2521
2594
 
@@ -2624,19 +2697,40 @@ if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
2624
2697
  #
2625
2698
 
2626
2699
  def _register_readable(self, fd: int) -> None:
2627
- self._control(fd, select.KQ_FILTER_READ, select.KQ_EV_ADD)
2700
+ self._update_registration(fd, 'read', 'add')
2628
2701
 
2629
2702
  def _register_writable(self, fd: int) -> None:
2630
- self._control(fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD)
2703
+ self._update_registration(fd, 'write', 'add')
2631
2704
 
2632
2705
  def _unregister_readable(self, fd: int) -> None:
2633
- self._control(fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)
2706
+ self._update_registration(fd, 'read', 'del')
2634
2707
 
2635
2708
  def _unregister_writable(self, fd: int) -> None:
2636
- self._control(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)
2709
+ self._update_registration(fd, 'write', 'del')
2710
+
2711
+ #
2637
2712
 
2638
- def _control(self, fd: int, filter: int, flags: int) -> None: # noqa
2639
- ke = select.kevent(fd, filter=filter, flags=flags)
2713
+ _CONTROL_FILTER_BY_READ_OR_WRITE: ta.ClassVar[ta.Mapping[ta.Literal['read', 'write'], int]] = {
2714
+ 'read': select.KQ_FILTER_READ,
2715
+ 'write': select.KQ_FILTER_WRITE,
2716
+ }
2717
+
2718
+ _CONTROL_FLAGS_BY_ADD_OR_DEL: ta.ClassVar[ta.Mapping[ta.Literal['add', 'del'], int]] = {
2719
+ 'add': select.KQ_EV_ADD,
2720
+ 'del': select.KQ_EV_DELETE,
2721
+ }
2722
+
2723
+ def _update_registration(
2724
+ self,
2725
+ fd: int,
2726
+ read_or_write: ta.Literal['read', 'write'],
2727
+ add_or_del: ta.Literal['add', 'del'],
2728
+ ) -> None: # noqa
2729
+ ke = select.kevent(
2730
+ fd,
2731
+ filter=self._CONTROL_FILTER_BY_READ_OR_WRITE[read_or_write],
2732
+ flags=self._CONTROL_FLAGS_BY_ADD_OR_DEL[add_or_del],
2733
+ )
2640
2734
  kq = self._get_kqueue()
2641
2735
  try:
2642
2736
  kq.control([ke], 0)
@@ -2647,7 +2741,8 @@ if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
2647
2741
  pass
2648
2742
  elif exc.errno == errno.ENOENT:
2649
2743
  # Can happen when trying to remove an already closed socket
2650
- pass
2744
+ if add_or_del == 'add':
2745
+ raise
2651
2746
  else:
2652
2747
  raise
2653
2748
 
@@ -5951,7 +6046,7 @@ class HasDispatchers(abc.ABC):
5951
6046
  class ProcessDispatcher(FdIoHandler, abc.ABC):
5952
6047
  @property
5953
6048
  @abc.abstractmethod
5954
- def channel(self) -> str:
6049
+ def channel(self) -> ProcessOutputChannel:
5955
6050
  raise NotImplementedError
5956
6051
 
5957
6052
  @property
@@ -6240,7 +6335,7 @@ class BaseProcessDispatcherImpl(ProcessDispatcher, abc.ABC):
6240
6335
  def __init__(
6241
6336
  self,
6242
6337
  process: Process,
6243
- channel: str,
6338
+ channel: ProcessOutputChannel,
6244
6339
  fd: Fd,
6245
6340
  *,
6246
6341
  event_callbacks: EventCallbacks,
@@ -6268,7 +6363,7 @@ class BaseProcessDispatcherImpl(ProcessDispatcher, abc.ABC):
6268
6363
  return self._process
6269
6364
 
6270
6365
  @property
6271
- def channel(self) -> str:
6366
+ def channel(self) -> ProcessOutputChannel:
6272
6367
  return self._channel
6273
6368
 
6274
6369
  def fd(self) -> Fd:
@@ -6515,7 +6610,7 @@ class ProcessInputDispatcherImpl(BaseProcessDispatcherImpl, ProcessInputDispatch
6515
6610
  def __init__(
6516
6611
  self,
6517
6612
  process: Process,
6518
- channel: str,
6613
+ channel: ProcessOutputChannel,
6519
6614
  fd: Fd,
6520
6615
  *,
6521
6616
  event_callbacks: EventCallbacks,
@@ -7024,9 +7119,9 @@ class IoManager(HasDispatchers):
7024
7119
  )
7025
7120
 
7026
7121
  timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
7027
- log.info(f'Polling: {timeout=}') # noqa
7122
+
7028
7123
  polled = self._poller.poll(timeout)
7029
- log.info(f'Polled: {polled=}') # noqa
7124
+
7030
7125
  if polled.msg is not None:
7031
7126
  log.error(polled.msg)
7032
7127
  if polled.exc is not None:
@@ -7151,6 +7246,8 @@ class HttpServer(HasDispatchers):
7151
7246
  self,
7152
7247
  handler: Handler,
7153
7248
  addr: Address = Address(('localhost', 8000)),
7249
+ *,
7250
+ exit_stack: contextlib.ExitStack,
7154
7251
  ) -> None:
7155
7252
  super().__init__()
7156
7253
 
@@ -7161,6 +7258,8 @@ class HttpServer(HasDispatchers):
7161
7258
 
7162
7259
  self._conns: ta.List[CoroHttpServerConnectionFdIoHandler] = []
7163
7260
 
7261
+ exit_stack.enter_context(defer(self._server.close)) # noqa
7262
+
7164
7263
  def get_dispatchers(self) -> Dispatchers:
7165
7264
  l = []
7166
7265
  for c in self._conns:
@@ -7205,6 +7304,7 @@ class SupervisorHttpHandler:
7205
7304
  'processes': {
7206
7305
  p.name: {
7207
7306
  'pid': p.pid,
7307
+ 'state': p.state.name,
7208
7308
  }
7209
7309
  for p in g
7210
7310
  },
@@ -7572,7 +7672,6 @@ class ProcessImpl(Process):
7572
7672
  self._last_stop = now
7573
7673
 
7574
7674
  if now > self._last_start:
7575
- log.info(f'{now - self._last_start=}') # noqa
7576
7675
  too_quickly = now - self._last_start < self._config.start_secs
7577
7676
  else:
7578
7677
  too_quickly = False
@@ -8227,11 +8326,29 @@ class Supervisor:
8227
8326
  #
8228
8327
 
8229
8328
  def _run_once(self) -> None:
8230
- now = time.time()
8231
- self._poll()
8232
- log.info(f'Poll took {time.time() - now}') # noqa
8329
+ if self._states.state < SupervisorState.RUNNING:
8330
+ if not self._stopping:
8331
+ # first time, set the stopping flag, do a notification and set stop_groups
8332
+ self._stopping = True
8333
+ self._stop_groups = sorted(self._process_groups)
8334
+ self._event_callbacks.notify(SupervisorStoppingEvent())
8335
+
8336
+ self._ordered_stop_groups_phase_1()
8337
+
8338
+ if not self.shutdown_report():
8339
+ # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
8340
+ raise ExitNow
8341
+
8342
+ self._io.poll()
8343
+
8344
+ for group in sorted(self._process_groups):
8345
+ for process in group:
8346
+ process.transition()
8347
+
8233
8348
  self._reap()
8349
+
8234
8350
  self._signal_handler.handle_signals()
8351
+
8235
8352
  self._tick()
8236
8353
 
8237
8354
  if self._states.state < SupervisorState.RUNNING:
@@ -8253,38 +8370,18 @@ class Supervisor:
8253
8370
  # down, so push it back on to the end of the stop group queue
8254
8371
  self._stop_groups.append(group)
8255
8372
 
8256
- def _poll(self) -> None:
8257
- sorted_groups = list(self._process_groups)
8258
- sorted_groups.sort()
8259
-
8260
- if self._states.state < SupervisorState.RUNNING:
8261
- if not self._stopping:
8262
- # first time, set the stopping flag, do a notification and set stop_groups
8263
- self._stopping = True
8264
- self._stop_groups = sorted_groups[:]
8265
- self._event_callbacks.notify(SupervisorStoppingEvent())
8266
-
8267
- self._ordered_stop_groups_phase_1()
8268
-
8269
- if not self.shutdown_report():
8270
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
8271
- raise ExitNow
8272
-
8273
- self._io.poll()
8274
-
8275
- for group in sorted_groups:
8276
- for process in group:
8277
- process.transition()
8373
+ #
8278
8374
 
8279
8375
  def _reap(self, *, once: bool = False, depth: int = 0) -> None:
8280
8376
  if depth >= 100:
8281
8377
  return
8282
8378
 
8283
8379
  wp = waitpid()
8284
- log.info(f'Waited pid: {wp}') # noqa
8380
+
8285
8381
  if wp is None or not wp.pid:
8286
8382
  return
8287
8383
 
8384
+ log.info(f'Waited pid: {wp}') # noqa
8288
8385
  process = self._pid_history.get(wp.pid, None)
8289
8386
  if process is None:
8290
8387
  _, msg = decode_wait_status(wp.sts)
@@ -8297,6 +8394,8 @@ class Supervisor:
8297
8394
  # keep reaping until no more kids to reap, but don't recurse infinitely
8298
8395
  self._reap(once=False, depth=depth + 1)
8299
8396
 
8397
+ #
8398
+
8300
8399
  def _tick(self, now: ta.Optional[float] = None) -> None:
8301
8400
  """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
8302
8401
 
@@ -8362,6 +8461,7 @@ class _FdIoPollerDaemonizeListener(DaemonizeListener):
8362
8461
 
8363
8462
 
8364
8463
  def bind_server(
8464
+ exit_stack: contextlib.ExitStack,
8365
8465
  config: ServerConfig,
8366
8466
  *,
8367
8467
  server_epoch: ta.Optional[ServerEpoch] = None,
@@ -8370,6 +8470,8 @@ def bind_server(
8370
8470
  lst: ta.List[InjectorBindingOrBindings] = [
8371
8471
  inj.bind(config),
8372
8472
 
8473
+ inj.bind(exit_stack),
8474
+
8373
8475
  inj.bind_array(DaemonizeListener),
8374
8476
  inj.bind_array_type(DaemonizeListener, DaemonizeListeners),
8375
8477
 
@@ -8425,8 +8527,10 @@ def bind_server(
8425
8527
  PollFdIoPoller,
8426
8528
  SelectFdIoPoller,
8427
8529
  ]))
8428
- lst.append(inj.bind(poller_impl, key=FdIoPoller, singleton=True))
8429
- inj.bind(_FdIoPollerDaemonizeListener, array=True, singleton=True)
8530
+ lst.extend([
8531
+ inj.bind(poller_impl, key=FdIoPoller, singleton=True),
8532
+ inj.bind(_FdIoPollerDaemonizeListener, array=True, singleton=True),
8533
+ ])
8430
8534
 
8431
8535
  #
8432
8536
 
@@ -8495,18 +8599,20 @@ def main(
8495
8599
  prepare=prepare_server_config,
8496
8600
  )
8497
8601
 
8498
- injector = inj.create_injector(bind_server(
8499
- config,
8500
- server_epoch=ServerEpoch(epoch),
8501
- inherited_fds=inherited_fds,
8502
- ))
8602
+ with contextlib.ExitStack() as es:
8603
+ injector = inj.create_injector(bind_server(
8604
+ es,
8605
+ config,
8606
+ server_epoch=ServerEpoch(epoch),
8607
+ inherited_fds=inherited_fds,
8608
+ ))
8503
8609
 
8504
- supervisor = injector[Supervisor]
8610
+ supervisor = injector[Supervisor]
8505
8611
 
8506
- try:
8507
- supervisor.main()
8508
- except ExitNow:
8509
- pass
8612
+ try:
8613
+ supervisor.main()
8614
+ except ExitNow:
8615
+ pass
8510
8616
 
8511
8617
  if supervisor.state < SupervisorState.RESTARTING:
8512
8618
  break
@@ -13,6 +13,7 @@ from .events import EventCallbacks
13
13
  from .events import ProcessCommunicationEvent
14
14
  from .events import ProcessLogStderrEvent
15
15
  from .events import ProcessLogStdoutEvent
16
+ from .events import ProcessOutputChannel
16
17
  from .types import Process
17
18
  from .types import ProcessDispatcher
18
19
  from .types import ProcessInputDispatcher
@@ -29,7 +30,7 @@ class BaseProcessDispatcherImpl(ProcessDispatcher, abc.ABC):
29
30
  def __init__(
30
31
  self,
31
32
  process: Process,
32
- channel: str,
33
+ channel: ProcessOutputChannel,
33
34
  fd: Fd,
34
35
  *,
35
36
  event_callbacks: EventCallbacks,
@@ -57,7 +58,7 @@ class BaseProcessDispatcherImpl(ProcessDispatcher, abc.ABC):
57
58
  return self._process
58
59
 
59
60
  @property
60
- def channel(self) -> str:
61
+ def channel(self) -> ProcessOutputChannel:
61
62
  return self._channel
62
63
 
63
64
  def fd(self) -> Fd:
@@ -304,7 +305,7 @@ class ProcessInputDispatcherImpl(BaseProcessDispatcherImpl, ProcessInputDispatch
304
305
  def __init__(
305
306
  self,
306
307
  process: Process,
307
- channel: str,
308
+ channel: ProcessOutputChannel,
308
309
  fd: Fd,
309
310
  *,
310
311
  event_callbacks: EventCallbacks,
@@ -8,6 +8,9 @@ from .states import ProcessState
8
8
  EventCallback = ta.Callable[['Event'], None]
9
9
 
10
10
 
11
+ ProcessOutputChannel = ta.Literal['stdout', 'stderr'] # ta.TypeAlias
12
+
13
+
11
14
  ##
12
15
 
13
16
 
@@ -43,7 +46,7 @@ class EventCallbacks:
43
46
 
44
47
 
45
48
  class ProcessLogEvent(Event, abc.ABC):
46
- channel: ta.Optional[str] = None
49
+ channel: ta.ClassVar[ProcessOutputChannel]
47
50
 
48
51
  def __init__(self, process, pid, data):
49
52
  super().__init__()
@@ -68,7 +71,7 @@ class ProcessCommunicationEvent(Event, abc.ABC):
68
71
  BEGIN_TOKEN = b'<!--XSUPERVISOR:BEGIN-->'
69
72
  END_TOKEN = b'<!--XSUPERVISOR:END-->'
70
73
 
71
- channel: ta.ClassVar[str]
74
+ channel: ta.ClassVar[ProcessOutputChannel]
72
75
 
73
76
  def __init__(self, process, pid, data):
74
77
  super().__init__()
@@ -1,9 +1,11 @@
1
1
  # ruff: noqa: U006 UP007
2
+ import contextlib
2
3
  import json
3
4
  import socket
4
5
  import typing as ta
5
6
 
6
7
  from omlish.lite.check import check_not_none
8
+ from omlish.lite.contextmanagers import defer
7
9
  from omlish.lite.fdio.corohttp import CoroHttpServerConnectionFdIoHandler
8
10
  from omlish.lite.fdio.handlers import SocketFdIoHandler
9
11
  from omlish.lite.http.handlers import HttpHandler
@@ -59,6 +61,8 @@ class HttpServer(HasDispatchers):
59
61
  self,
60
62
  handler: Handler,
61
63
  addr: Address = Address(('localhost', 8000)),
64
+ *,
65
+ exit_stack: contextlib.ExitStack,
62
66
  ) -> None:
63
67
  super().__init__()
64
68
 
@@ -69,6 +73,8 @@ class HttpServer(HasDispatchers):
69
73
 
70
74
  self._conns: ta.List[CoroHttpServerConnectionFdIoHandler] = []
71
75
 
76
+ exit_stack.enter_context(defer(self._server.close)) # noqa
77
+
72
78
  def get_dispatchers(self) -> Dispatchers:
73
79
  l = []
74
80
  for c in self._conns:
@@ -113,6 +119,7 @@ class SupervisorHttpHandler:
113
119
  'processes': {
114
120
  p.name: {
115
121
  'pid': p.pid,
122
+ 'state': p.state.name,
116
123
  }
117
124
  for p in g
118
125
  },
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ import contextlib
2
3
  import dataclasses as dc
3
4
  import typing as ta
4
5
 
@@ -56,6 +57,7 @@ class _FdIoPollerDaemonizeListener(DaemonizeListener):
56
57
 
57
58
 
58
59
  def bind_server(
60
+ exit_stack: contextlib.ExitStack,
59
61
  config: ServerConfig,
60
62
  *,
61
63
  server_epoch: ta.Optional[ServerEpoch] = None,
@@ -64,6 +66,8 @@ def bind_server(
64
66
  lst: ta.List[InjectorBindingOrBindings] = [
65
67
  inj.bind(config),
66
68
 
69
+ inj.bind(exit_stack),
70
+
67
71
  inj.bind_array(DaemonizeListener),
68
72
  inj.bind_array_type(DaemonizeListener, DaemonizeListeners),
69
73
 
@@ -119,8 +123,10 @@ def bind_server(
119
123
  PollFdIoPoller,
120
124
  SelectFdIoPoller,
121
125
  ]))
122
- lst.append(inj.bind(poller_impl, key=FdIoPoller, singleton=True))
123
- inj.bind(_FdIoPollerDaemonizeListener, array=True, singleton=True)
126
+ lst.extend([
127
+ inj.bind(poller_impl, key=FdIoPoller, singleton=True),
128
+ inj.bind(_FdIoPollerDaemonizeListener, array=True, singleton=True),
129
+ ])
124
130
 
125
131
  #
126
132
 
ominfra/supervisor/io.py CHANGED
@@ -44,9 +44,9 @@ class IoManager(HasDispatchers):
44
44
  )
45
45
 
46
46
  timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
47
- log.info(f'Polling: {timeout=}') # noqa
47
+
48
48
  polled = self._poller.poll(timeout)
49
- log.info(f'Polled: {polled=}') # noqa
49
+
50
50
  if polled.msg is not None:
51
51
  log.error(polled.msg)
52
52
  if polled.exc is not None:
@@ -29,6 +29,7 @@
29
29
  # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
30
30
  # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
31
31
  # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ import contextlib
32
33
  import itertools
33
34
  import os.path
34
35
  import typing as ta
@@ -96,18 +97,20 @@ def main(
96
97
  prepare=prepare_server_config,
97
98
  )
98
99
 
99
- injector = inj.create_injector(bind_server(
100
- config,
101
- server_epoch=ServerEpoch(epoch),
102
- inherited_fds=inherited_fds,
103
- ))
100
+ with contextlib.ExitStack() as es:
101
+ injector = inj.create_injector(bind_server(
102
+ es,
103
+ config,
104
+ server_epoch=ServerEpoch(epoch),
105
+ inherited_fds=inherited_fds,
106
+ ))
104
107
 
105
- supervisor = injector[Supervisor]
108
+ supervisor = injector[Supervisor]
106
109
 
107
- try:
108
- supervisor.main()
109
- except ExitNow:
110
- pass
110
+ try:
111
+ supervisor.main()
112
+ except ExitNow:
113
+ pass
111
114
 
112
115
  if supervisor.state < SupervisorState.RESTARTING:
113
116
  break
@@ -378,7 +378,6 @@ class ProcessImpl(Process):
378
378
  self._last_stop = now
379
379
 
380
380
  if now > self._last_start:
381
- log.info(f'{now - self._last_start=}') # noqa
382
381
  too_quickly = now - self._last_start < self._config.start_secs
383
382
  else:
384
383
  too_quickly = False
@@ -27,6 +27,17 @@ class ProcessState(enum.IntEnum):
27
27
  return self in SIGNALABLE_STATES
28
28
 
29
29
 
30
+ # http://supervisord.org/subprocess.html
31
+ STATE_TRANSITIONS = {
32
+ ProcessState.STOPPED: (ProcessState.STARTING,),
33
+ ProcessState.STARTING: (ProcessState.RUNNING, ProcessState.BACKOFF, ProcessState.STOPPING),
34
+ ProcessState.RUNNING: (ProcessState.STOPPING, ProcessState.EXITED),
35
+ ProcessState.BACKOFF: (ProcessState.STARTING, ProcessState.FATAL),
36
+ ProcessState.STOPPING: (ProcessState.STOPPED,),
37
+ ProcessState.EXITED: (ProcessState.STARTING,),
38
+ ProcessState.FATAL: (ProcessState.STARTING,),
39
+ }
40
+
30
41
  STOPPED_STATES = (
31
42
  ProcessState.STOPPED,
32
43
  ProcessState.EXITED,
@@ -182,11 +182,29 @@ class Supervisor:
182
182
  #
183
183
 
184
184
  def _run_once(self) -> None:
185
- now = time.time()
186
- self._poll()
187
- log.info(f'Poll took {time.time() - now}') # noqa
185
+ if self._states.state < SupervisorState.RUNNING:
186
+ if not self._stopping:
187
+ # first time, set the stopping flag, do a notification and set stop_groups
188
+ self._stopping = True
189
+ self._stop_groups = sorted(self._process_groups)
190
+ self._event_callbacks.notify(SupervisorStoppingEvent())
191
+
192
+ self._ordered_stop_groups_phase_1()
193
+
194
+ if not self.shutdown_report():
195
+ # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
196
+ raise ExitNow
197
+
198
+ self._io.poll()
199
+
200
+ for group in sorted(self._process_groups):
201
+ for process in group:
202
+ process.transition()
203
+
188
204
  self._reap()
205
+
189
206
  self._signal_handler.handle_signals()
207
+
190
208
  self._tick()
191
209
 
192
210
  if self._states.state < SupervisorState.RUNNING:
@@ -208,38 +226,18 @@ class Supervisor:
208
226
  # down, so push it back on to the end of the stop group queue
209
227
  self._stop_groups.append(group)
210
228
 
211
- def _poll(self) -> None:
212
- sorted_groups = list(self._process_groups)
213
- sorted_groups.sort()
214
-
215
- if self._states.state < SupervisorState.RUNNING:
216
- if not self._stopping:
217
- # first time, set the stopping flag, do a notification and set stop_groups
218
- self._stopping = True
219
- self._stop_groups = sorted_groups[:]
220
- self._event_callbacks.notify(SupervisorStoppingEvent())
221
-
222
- self._ordered_stop_groups_phase_1()
223
-
224
- if not self.shutdown_report():
225
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
226
- raise ExitNow
227
-
228
- self._io.poll()
229
-
230
- for group in sorted_groups:
231
- for process in group:
232
- process.transition()
229
+ #
233
230
 
234
231
  def _reap(self, *, once: bool = False, depth: int = 0) -> None:
235
232
  if depth >= 100:
236
233
  return
237
234
 
238
235
  wp = waitpid()
239
- log.info(f'Waited pid: {wp}') # noqa
236
+
240
237
  if wp is None or not wp.pid:
241
238
  return
242
239
 
240
+ log.info(f'Waited pid: {wp}') # noqa
243
241
  process = self._pid_history.get(wp.pid, None)
244
242
  if process is None:
245
243
  _, msg = decode_wait_status(wp.sts)
@@ -252,6 +250,8 @@ class Supervisor:
252
250
  # keep reaping until no more kids to reap, but don't recurse infinitely
253
251
  self._reap(once=False, depth=depth + 1)
254
252
 
253
+ #
254
+
255
255
  def _tick(self, now: ta.Optional[float] = None) -> None:
256
256
  """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
257
257
 
@@ -7,6 +7,7 @@ from omlish.lite.fdio.handlers import FdIoHandler
7
7
 
8
8
  from .configs import ProcessConfig
9
9
  from .configs import ProcessGroupConfig
10
+ from .events import ProcessOutputChannel
10
11
  from .states import ProcessState
11
12
  from .states import SupervisorState
12
13
  from .utils.collections import KeyedCollectionAccessors
@@ -71,7 +72,7 @@ class HasDispatchers(abc.ABC):
71
72
  class ProcessDispatcher(FdIoHandler, abc.ABC):
72
73
  @property
73
74
  @abc.abstractmethod
74
- def channel(self) -> str:
75
+ def channel(self) -> ProcessOutputChannel:
75
76
  raise NotImplementedError
76
77
 
77
78
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev131
3
+ Version: 0.0.0.dev132
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev131
16
- Requires-Dist: omlish==0.0.0.dev131
15
+ Requires-Dist: omdev==0.0.0.dev132
16
+ Requires-Dist: omlish==0.0.0.dev132
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"
@@ -60,34 +60,34 @@ ominfra/pyremote/_runcommands.py,sha256=RKNvaHje-QGpvwwmFQ6OfEn3eEJ2c6zVUBYnDo2R
60
60
  ominfra/pyremote/bootstrap.py,sha256=RvMO3YGaN1E4sgUi1JEtiPak8cjvqtc_vRCq1yqbeZg,3370
61
61
  ominfra/pyremote/runcommands.py,sha256=bviS0_TDIoZVAe4h-_iavbvJtVSFu8lnk7fQ5iasCWE,1571
62
62
  ominfra/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- ominfra/scripts/journald2aws.py,sha256=qNGdiXxng21BBO5kqOQvnOf1Ftc3rTJZXpCu8MO-Kvc,131387
64
- ominfra/scripts/supervisor.py,sha256=a0oz88cW-9y0P_LNFXuDBKLgDywv3RIUxxXuZ8VezCE,242615
63
+ ominfra/scripts/journald2aws.py,sha256=o6WgI9R3MXdH4-BKUYAK2pcu3E5T0GAl5pggzR4Lhrk,131537
64
+ ominfra/scripts/supervisor.py,sha256=_n84Wz_L1QZmOTD3S9PGecmSt6OUPCO6K9lOu4VoByE,245533
65
65
  ominfra/supervisor/LICENSE.txt,sha256=yvqaMNsDhWxziHa9ien6qCW1SkZv-DQlAg96XjfSee8,1746
66
66
  ominfra/supervisor/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
67
67
  ominfra/supervisor/__main__.py,sha256=I0yFw-C08OOiZ3BF6lF1Oiv789EQXu-_j6whDhQUTEA,66
68
68
  ominfra/supervisor/configs.py,sha256=OPtiL5_SjBYQ9xDqMgZvjoXgpleTxv4d3C4fD1X2Dz0,3966
69
69
  ominfra/supervisor/dispatchers.py,sha256=dfjog5PyVAJaFzgFI0tpy38ZgDiFrewMjVi21ksrlAg,1007
70
- ominfra/supervisor/dispatchersimpl.py,sha256=dZpfG7Z2_AcRpfa2RaZ-xolsbM5iKkifdQpON7w7Upo,11257
71
- ominfra/supervisor/events.py,sha256=w3HQFrq-SuroYWoQfNFYeU1phnTvHTgsAqA6TGtAafI,6593
70
+ ominfra/supervisor/dispatchersimpl.py,sha256=q3dEyOHWTPKm28nmAGisjgIW1BX6O3-SzbYa7nWuTEs,11349
71
+ ominfra/supervisor/events.py,sha256=XGrtzHr1xm0dwjz329fn9eR0_Ap-LQL6Sk8LJ8eVDEo,6692
72
72
  ominfra/supervisor/exceptions.py,sha256=Qbu211H3CLlSmi9LsSikOwrcL5HgJP9ugvcKWlGTAoI,750
73
73
  ominfra/supervisor/groups.py,sha256=MBbsbt8Zh_WEYkGOr1KXa82gnPVw9wPB2lAnqhugXSc,2457
74
74
  ominfra/supervisor/groupsimpl.py,sha256=nIrW4SmB0W6c2jOR_HhkfVcH4eHyLZnG1FJ0MCzc6mQ,2292
75
- ominfra/supervisor/http.py,sha256=y0tos6zbb4k-WvNQXlgAeK1qi5mKXkyXO9bVJt4OXew,3227
76
- ominfra/supervisor/inject.py,sha256=onKUudx5eBbNuXwEqeiaVIIA3ZbpUSDiEAKg9GBiWG8,4514
77
- ominfra/supervisor/io.py,sha256=2NO4BYC-PznIrJpTFxN8UEAhd_codfNm_HI424gYQ3c,3294
78
- ominfra/supervisor/main.py,sha256=ebe7skFPfwXV2meMVRndhuLZmz-LiuHH1x1CgiarR0o,4132
75
+ ominfra/supervisor/http.py,sha256=WC7TD6ECD28GU83sUAd6Xau2pvYyZZmmFDgL-E4_Jd0,3472
76
+ ominfra/supervisor/inject.py,sha256=cH7ZV5KnOlHh90_S6YWlJsJCfM9Ve956GzEAhl4arx8,4627
77
+ ominfra/supervisor/io.py,sha256=_G66luJDF-i1JPM-qJiREMdmHk2ul7n-kjr7ex1sF6s,3200
78
+ ominfra/supervisor/main.py,sha256=oqaWOcnHJgaxNhjyphPgjaNjHPjDcx7kzYMjtZpwSxE,4253
79
79
  ominfra/supervisor/pipes.py,sha256=2ZihNTnRNjnIPOtPbm3_pyqO15f7BNs7WnNtO5V8ahM,2231
80
80
  ominfra/supervisor/privileges.py,sha256=kaRTHI7XjqzxEWCeHp3_0J0Vc4gSPugRbXEwxuw6MYE,2054
81
81
  ominfra/supervisor/process.py,sha256=UaubVxsxVqDnbuWVpTH0DTGbJGLO0vGJ9mNcvy2kCXM,217
82
- ominfra/supervisor/processimpl.py,sha256=YRHIVYdPXighBXAVNmW3ur7IhoSxhkxFYCQI1B8CpDo,18748
82
+ ominfra/supervisor/processimpl.py,sha256=BcvX-e03qjFSUogDUVmevwyizXS3kss3JY1QkHWjKWE,18689
83
83
  ominfra/supervisor/setup.py,sha256=7HwwwI-WT_Z0WjZ9_l5Orr4K298nKKhQ1f_ZgGsi9TU,622
84
84
  ominfra/supervisor/setupimpl.py,sha256=88h3oYsdJ0LAo7sZZZGRQti14oQay3b-0Vd_h3Cl108,9638
85
85
  ominfra/supervisor/signals.py,sha256=jY52naUifcAjd6nICTP1ZW3IQSPsHB4cvbsJo8_QV_U,2196
86
86
  ominfra/supervisor/spawning.py,sha256=i1k3tmqWyU-KIN7kel-JVxTVGnLiTIVmZzlstJSZpjM,622
87
87
  ominfra/supervisor/spawningimpl.py,sha256=Pkp6mefJhOGCCj5T2I1jXEsVp15g9KMKbANYgoX-ws8,11162
88
- ominfra/supervisor/states.py,sha256=9yoNOSwalRcKEnCP9zG6tVS0oivo5tCeuH6AaaW7Jpc,890
89
- ominfra/supervisor/supervisor.py,sha256=Ryvs80cDOe4pvBdZazLLaOCR1We0aj44Y1Hw4l3FUnk,9514
90
- ominfra/supervisor/types.py,sha256=RamAYEF3fISfKwlIMRleLOFuKVWrxjQJGI6p76jK03c,3959
88
+ ominfra/supervisor/states.py,sha256=x1trJQbItkSegOmotpn5YNcZMLbBL8I3GmhvFCQl4Oo,1400
89
+ ominfra/supervisor/supervisor.py,sha256=5V5DJHThPzJmctO4_UZV5-vx0ieT43TIcL_ZIl6T0xQ,9342
90
+ ominfra/supervisor/types.py,sha256=n4AILLzdeavM5ejOFKP-rPRNK87UV8aFyoeRKPVDxxg,4017
91
91
  ominfra/supervisor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  ominfra/supervisor/utils/collections.py,sha256=vcfmVYS4QngMdtEI1DvdRIcubmy55Wj40NCzW27_rIY,1361
93
93
  ominfra/supervisor/utils/diag.py,sha256=ujz4gkW7p3wmbaKFM8Hz5eHEwpoUkbB8JeDvcHilCz0,705
@@ -103,9 +103,9 @@ ominfra/tailscale/api.py,sha256=C5-t_b6jZXUWcy5k8bXm7CFnk73pSdrlMOgGDeGVrpw,1370
103
103
  ominfra/tailscale/cli.py,sha256=DSGp4hn5xwOW-l_u_InKlSF6kIobxtUtVssf_73STs0,3567
104
104
  ominfra/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
105
  ominfra/tools/listresources.py,sha256=4qVg5txsb10EHhvqXXeM6gJ2jx9LbroEnPydDv1uXs0,6176
106
- ominfra-0.0.0.dev131.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
107
- ominfra-0.0.0.dev131.dist-info/METADATA,sha256=tX48aF4l92ov4-dQc50yn6pbYrXTAz3j73ZJS7H9p04,731
108
- ominfra-0.0.0.dev131.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
109
- ominfra-0.0.0.dev131.dist-info/entry_points.txt,sha256=kgecQ2MgGrM9qK744BoKS3tMesaC3yjLnl9pa5CRczg,37
110
- ominfra-0.0.0.dev131.dist-info/top_level.txt,sha256=E-b2OHkk_AOBLXHYZQ2EOFKl-_6uOGd8EjeG-Zy6h_w,8
111
- ominfra-0.0.0.dev131.dist-info/RECORD,,
106
+ ominfra-0.0.0.dev132.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
107
+ ominfra-0.0.0.dev132.dist-info/METADATA,sha256=vq41f4P1jKGnX9UuGdMibA-bayJhVbb8rk_vZQw0-r8,731
108
+ ominfra-0.0.0.dev132.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
109
+ ominfra-0.0.0.dev132.dist-info/entry_points.txt,sha256=kgecQ2MgGrM9qK744BoKS3tMesaC3yjLnl9pa5CRczg,37
110
+ ominfra-0.0.0.dev132.dist-info/top_level.txt,sha256=E-b2OHkk_AOBLXHYZQ2EOFKl-_6uOGd8EjeG-Zy6h_w,8
111
+ ominfra-0.0.0.dev132.dist-info/RECORD,,