ominfra 0.0.0.dev128__py3-none-any.whl → 0.0.0.dev129__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.
@@ -1494,6 +1494,30 @@ def check_not_equal(l: T, r: T) -> T:
1494
1494
  return l
1495
1495
 
1496
1496
 
1497
+ def check_is(l: T, r: T) -> T:
1498
+ if l is not r:
1499
+ raise ValueError(l, r)
1500
+ return l
1501
+
1502
+
1503
+ def check_is_not(l: T, r: ta.Any) -> T:
1504
+ if l is r:
1505
+ raise ValueError(l, r)
1506
+ return l
1507
+
1508
+
1509
+ def check_in(v: T, c: ta.Container[T]) -> T:
1510
+ if v not in c:
1511
+ raise ValueError(v, c)
1512
+ return v
1513
+
1514
+
1515
+ def check_not_in(v: T, c: ta.Container[T]) -> T:
1516
+ if v in c:
1517
+ raise ValueError(v, c)
1518
+ return v
1519
+
1520
+
1497
1521
  def check_single(vs: ta.Iterable[T]) -> T:
1498
1522
  [v] = vs
1499
1523
  return v
@@ -2604,6 +2628,11 @@ class HttpRequestParser:
2604
2628
 
2605
2629
  ########################################
2606
2630
  # ../../../omlish/lite/inject.py
2631
+ """
2632
+ TODO:
2633
+ - recursion detection
2634
+ - bind empty array
2635
+ """
2607
2636
 
2608
2637
 
2609
2638
  ###
@@ -2703,7 +2732,7 @@ class InjectorError(Exception):
2703
2732
  pass
2704
2733
 
2705
2734
 
2706
- @dc.dataclass(frozen=True)
2735
+ @dc.dataclass()
2707
2736
  class InjectorKeyError(InjectorError):
2708
2737
  key: InjectorKey
2709
2738
 
@@ -2711,16 +2740,18 @@ class InjectorKeyError(InjectorError):
2711
2740
  name: ta.Optional[str] = None
2712
2741
 
2713
2742
 
2714
- @dc.dataclass(frozen=True)
2715
2743
  class UnboundInjectorKeyError(InjectorKeyError):
2716
2744
  pass
2717
2745
 
2718
2746
 
2719
- @dc.dataclass(frozen=True)
2720
2747
  class DuplicateInjectorKeyError(InjectorKeyError):
2721
2748
  pass
2722
2749
 
2723
2750
 
2751
+ class CyclicDependencyInjectorKeyError(InjectorKeyError):
2752
+ pass
2753
+
2754
+
2724
2755
  ###
2725
2756
  # keys
2726
2757
 
@@ -3054,22 +3085,65 @@ class _Injector(Injector):
3054
3085
  if _INJECTOR_INJECTOR_KEY in self._pfm:
3055
3086
  raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
3056
3087
 
3088
+ self.__cur_req: ta.Optional[_Injector._Request] = None
3089
+
3090
+ class _Request:
3091
+ def __init__(self, injector: '_Injector') -> None:
3092
+ super().__init__()
3093
+ self._injector = injector
3094
+ self._provisions: ta.Dict[InjectorKey, Maybe] = {}
3095
+ self._seen_keys: ta.Set[InjectorKey] = set()
3096
+
3097
+ def handle_key(self, key: InjectorKey) -> Maybe[Maybe]:
3098
+ try:
3099
+ return Maybe.just(self._provisions[key])
3100
+ except KeyError:
3101
+ pass
3102
+ if key in self._seen_keys:
3103
+ raise CyclicDependencyInjectorKeyError(key)
3104
+ self._seen_keys.add(key)
3105
+ return Maybe.empty()
3106
+
3107
+ def handle_provision(self, key: InjectorKey, mv: Maybe) -> Maybe:
3108
+ check_in(key, self._seen_keys)
3109
+ check_not_in(key, self._provisions)
3110
+ self._provisions[key] = mv
3111
+ return mv
3112
+
3113
+ @contextlib.contextmanager
3114
+ def _current_request(self) -> ta.Generator[_Request, None, None]:
3115
+ if (cr := self.__cur_req) is not None:
3116
+ yield cr
3117
+ return
3118
+
3119
+ cr = self._Request(self)
3120
+ try:
3121
+ self.__cur_req = cr
3122
+ yield cr
3123
+ finally:
3124
+ self.__cur_req = None
3125
+
3057
3126
  def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
3058
3127
  key = as_injector_key(key)
3059
3128
 
3060
- if key == _INJECTOR_INJECTOR_KEY:
3061
- return Maybe.just(self)
3129
+ cr: _Injector._Request
3130
+ with self._current_request() as cr:
3131
+ if (rv := cr.handle_key(key)).present:
3132
+ return rv.must()
3133
+
3134
+ if key == _INJECTOR_INJECTOR_KEY:
3135
+ return cr.handle_provision(key, Maybe.just(self))
3062
3136
 
3063
- fn = self._pfm.get(key)
3064
- if fn is not None:
3065
- return Maybe.just(fn(self))
3137
+ fn = self._pfm.get(key)
3138
+ if fn is not None:
3139
+ return cr.handle_provision(key, Maybe.just(fn(self)))
3066
3140
 
3067
- if self._p is not None:
3068
- pv = self._p.try_provide(key)
3069
- if pv is not None:
3070
- return Maybe.empty()
3141
+ if self._p is not None:
3142
+ pv = self._p.try_provide(key)
3143
+ if pv is not None:
3144
+ return cr.handle_provision(key, Maybe.empty())
3071
3145
 
3072
- return Maybe.empty()
3146
+ return cr.handle_provision(key, Maybe.empty())
3073
3147
 
3074
3148
  def provide(self, key: ta.Any) -> ta.Any:
3075
3149
  v = self.try_provide(key)
@@ -4158,6 +4232,23 @@ def unmarshal_obj(o: ta.Any, ty: ta.Union[ta.Type[T], ta.Any]) -> T:
4158
4232
  return get_obj_marshaler(ty).unmarshal(o)
4159
4233
 
4160
4234
 
4235
+ ########################################
4236
+ # ../../../omlish/lite/runtime.py
4237
+
4238
+
4239
+ @cached_nullary
4240
+ def is_debugger_attached() -> bool:
4241
+ return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
4242
+
4243
+
4244
+ REQUIRED_PYTHON_VERSION = (3, 8)
4245
+
4246
+
4247
+ def check_runtime_version() -> None:
4248
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
4249
+ raise OSError(f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
4250
+
4251
+
4161
4252
  ########################################
4162
4253
  # ../../configs.py
4163
4254
 
@@ -5310,6 +5401,10 @@ class CoroHttpServerSocketHandler(SocketHandler):
5310
5401
  ##
5311
5402
 
5312
5403
 
5404
+ class ExitNow(Exception): # noqa
5405
+ pass
5406
+
5407
+
5313
5408
  ServerEpoch = ta.NewType('ServerEpoch', int)
5314
5409
 
5315
5410
 
@@ -5333,12 +5428,7 @@ class ConfigPriorityOrdered(abc.ABC):
5333
5428
  ##
5334
5429
 
5335
5430
 
5336
- class ServerContext(abc.ABC):
5337
- @property
5338
- @abc.abstractmethod
5339
- def config(self) -> ServerConfig:
5340
- raise NotImplementedError
5341
-
5431
+ class SupervisorStateManager(abc.ABC):
5342
5432
  @property
5343
5433
  @abc.abstractmethod
5344
5434
  def state(self) -> SupervisorState:
@@ -5353,11 +5443,6 @@ class ServerContext(abc.ABC):
5353
5443
 
5354
5444
 
5355
5445
  class Dispatcher(abc.ABC):
5356
- @property
5357
- @abc.abstractmethod
5358
- def process(self) -> 'Process':
5359
- raise NotImplementedError
5360
-
5361
5446
  @property
5362
5447
  @abc.abstractmethod
5363
5448
  def channel(self) -> str:
@@ -5401,8 +5486,32 @@ class Dispatcher(abc.ABC):
5401
5486
  def handle_write_event(self) -> None:
5402
5487
  raise TypeError
5403
5488
 
5489
+ #
5490
+
5491
+ def handle_connect(self) -> None:
5492
+ raise TypeError
5493
+
5494
+ def handle_close(self) -> None:
5495
+ raise TypeError
5496
+
5497
+ def handle_accepted(self, sock, addr) -> None:
5498
+ raise TypeError
5499
+
5500
+
5501
+ class HasDispatchers(abc.ABC):
5502
+ @abc.abstractmethod
5503
+ def get_dispatchers(self) -> 'Dispatchers':
5504
+ raise NotImplementedError
5505
+
5404
5506
 
5405
- class OutputDispatcher(Dispatcher, abc.ABC):
5507
+ class ProcessDispatcher(Dispatcher, abc.ABC):
5508
+ @property
5509
+ @abc.abstractmethod
5510
+ def process(self) -> 'Process':
5511
+ raise NotImplementedError
5512
+
5513
+
5514
+ class ProcessOutputDispatcher(ProcessDispatcher, abc.ABC):
5406
5515
  @abc.abstractmethod
5407
5516
  def remove_logs(self) -> None:
5408
5517
  raise NotImplementedError
@@ -5412,7 +5521,7 @@ class OutputDispatcher(Dispatcher, abc.ABC):
5412
5521
  raise NotImplementedError
5413
5522
 
5414
5523
 
5415
- class InputDispatcher(Dispatcher, abc.ABC):
5524
+ class ProcessInputDispatcher(ProcessDispatcher, abc.ABC):
5416
5525
  @abc.abstractmethod
5417
5526
  def write(self, chars: ta.Union[bytes, str]) -> None:
5418
5527
  raise NotImplementedError
@@ -5425,7 +5534,11 @@ class InputDispatcher(Dispatcher, abc.ABC):
5425
5534
  ##
5426
5535
 
5427
5536
 
5428
- class Process(ConfigPriorityOrdered, abc.ABC):
5537
+ class Process(
5538
+ ConfigPriorityOrdered,
5539
+ HasDispatchers,
5540
+ abc.ABC,
5541
+ ):
5429
5542
  @property
5430
5543
  @abc.abstractmethod
5431
5544
  def name(self) -> str:
@@ -5448,11 +5561,6 @@ class Process(ConfigPriorityOrdered, abc.ABC):
5448
5561
 
5449
5562
  #
5450
5563
 
5451
- @property
5452
- @abc.abstractmethod
5453
- def context(self) -> ServerContext:
5454
- raise NotImplementedError
5455
-
5456
5564
  @abc.abstractmethod
5457
5565
  def finish(self, sts: Rc) -> None:
5458
5566
  raise NotImplementedError
@@ -5469,18 +5577,15 @@ class Process(ConfigPriorityOrdered, abc.ABC):
5469
5577
  def transition(self) -> None:
5470
5578
  raise NotImplementedError
5471
5579
 
5580
+ @property
5472
5581
  @abc.abstractmethod
5473
- def get_state(self) -> ProcessState:
5582
+ def state(self) -> ProcessState:
5474
5583
  raise NotImplementedError
5475
5584
 
5476
5585
  @abc.abstractmethod
5477
5586
  def after_setuid(self) -> None:
5478
5587
  raise NotImplementedError
5479
5588
 
5480
- @abc.abstractmethod
5481
- def get_dispatchers(self) -> 'Dispatchers':
5482
- raise NotImplementedError
5483
-
5484
5589
 
5485
5590
  ##
5486
5591
 
@@ -5520,75 +5625,6 @@ class ProcessGroup(
5520
5625
  raise NotImplementedError
5521
5626
 
5522
5627
 
5523
- ########################################
5524
- # ../context.py
5525
-
5526
-
5527
- class ServerContextImpl(ServerContext):
5528
- def __init__(
5529
- self,
5530
- config: ServerConfig,
5531
- poller: Poller,
5532
- *,
5533
- epoch: ServerEpoch = ServerEpoch(0),
5534
- ) -> None:
5535
- super().__init__()
5536
-
5537
- self._config = config
5538
- self._poller = poller
5539
- self._epoch = epoch
5540
-
5541
- self._state: SupervisorState = SupervisorState.RUNNING
5542
-
5543
- @property
5544
- def config(self) -> ServerConfig:
5545
- return self._config
5546
-
5547
- @property
5548
- def epoch(self) -> ServerEpoch:
5549
- return self._epoch
5550
-
5551
- @property
5552
- def first(self) -> bool:
5553
- return not self._epoch
5554
-
5555
- @property
5556
- def state(self) -> SupervisorState:
5557
- return self._state
5558
-
5559
- def set_state(self, state: SupervisorState) -> None:
5560
- self._state = state
5561
-
5562
- #
5563
-
5564
- def waitpid(self) -> ta.Tuple[ta.Optional[Pid], ta.Optional[Rc]]:
5565
- # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
5566
- # still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
5567
- # waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
5568
- # normal course in the mainloop), we'll eventually reap the child that we tried to reap during the interrupted
5569
- # call. At least on Linux, this appears to be true, or at least stopping 50 processes at once never left zombies
5570
- # lying around.
5571
- try:
5572
- pid, sts = os.waitpid(-1, os.WNOHANG)
5573
- except OSError as exc:
5574
- code = exc.args[0]
5575
- if code not in (errno.ECHILD, errno.EINTR):
5576
- log.critical('waitpid error %r; a process may not be cleaned up properly', code)
5577
- if code == errno.EINTR:
5578
- log.debug('EINTR during reap')
5579
- pid, sts = None, None
5580
- return pid, sts # type: ignore
5581
-
5582
- def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
5583
- prefix = f'{name}-{channel}---{identifier}-'
5584
- logfile = mktempfile(
5585
- suffix='.log',
5586
- prefix=prefix,
5587
- dir=self.config.child_logdir,
5588
- )
5589
- return logfile
5590
-
5591
-
5592
5628
  ########################################
5593
5629
  # ../dispatchers.py
5594
5630
 
@@ -5612,12 +5648,12 @@ class Dispatchers(KeyedCollection[Fd, Dispatcher]):
5612
5648
 
5613
5649
  def remove_logs(self) -> None:
5614
5650
  for d in self:
5615
- if isinstance(d, OutputDispatcher):
5651
+ if isinstance(d, ProcessOutputDispatcher):
5616
5652
  d.remove_logs()
5617
5653
 
5618
5654
  def reopen_logs(self) -> None:
5619
5655
  for d in self:
5620
- if isinstance(d, OutputDispatcher):
5656
+ if isinstance(d, ProcessOutputDispatcher):
5621
5657
  d.reopen_logs()
5622
5658
 
5623
5659
 
@@ -5625,7 +5661,7 @@ class Dispatchers(KeyedCollection[Fd, Dispatcher]):
5625
5661
  # ../dispatchersimpl.py
5626
5662
 
5627
5663
 
5628
- class BaseDispatcherImpl(Dispatcher, abc.ABC):
5664
+ class BaseProcessDispatcherImpl(ProcessDispatcher, abc.ABC):
5629
5665
  def __init__(
5630
5666
  self,
5631
5667
  process: Process,
@@ -5633,6 +5669,7 @@ class BaseDispatcherImpl(Dispatcher, abc.ABC):
5633
5669
  fd: Fd,
5634
5670
  *,
5635
5671
  event_callbacks: EventCallbacks,
5672
+ server_config: ServerConfig,
5636
5673
  ) -> None:
5637
5674
  super().__init__()
5638
5675
 
@@ -5640,6 +5677,7 @@ class BaseDispatcherImpl(Dispatcher, abc.ABC):
5640
5677
  self._channel = channel # 'stderr' or 'stdout'
5641
5678
  self._fd = fd
5642
5679
  self._event_callbacks = event_callbacks
5680
+ self._server_config = server_config
5643
5681
 
5644
5682
  self._closed = False # True if close() has been called
5645
5683
 
@@ -5680,7 +5718,7 @@ class BaseDispatcherImpl(Dispatcher, abc.ABC):
5680
5718
  self.close()
5681
5719
 
5682
5720
 
5683
- class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5721
+ class ProcessOutputDispatcherImpl(BaseProcessDispatcherImpl, ProcessOutputDispatcher):
5684
5722
  """
5685
5723
  Dispatcher for one channel (stdout or stderr) of one process. Serves several purposes:
5686
5724
 
@@ -5696,12 +5734,14 @@ class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5696
5734
  fd: Fd,
5697
5735
  *,
5698
5736
  event_callbacks: EventCallbacks,
5737
+ server_config: ServerConfig,
5699
5738
  ) -> None:
5700
5739
  super().__init__(
5701
5740
  process,
5702
5741
  event_type.channel,
5703
5742
  fd,
5704
5743
  event_callbacks=event_callbacks,
5744
+ server_config=server_config,
5705
5745
  )
5706
5746
 
5707
5747
  self._event_type = event_type
@@ -5725,11 +5765,10 @@ class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5725
5765
 
5726
5766
  self._main_log_level = logging.DEBUG
5727
5767
 
5728
- self._log_to_main_log = process.context.config.loglevel <= self._main_log_level
5768
+ self._log_to_main_log = self._server_config.loglevel <= self._main_log_level
5729
5769
 
5730
- config = self._process.config
5731
- self._stdout_events_enabled = config.stdout.events_enabled
5732
- self._stderr_events_enabled = config.stderr.events_enabled
5770
+ self._stdout_events_enabled = self._process.config.stdout.events_enabled
5771
+ self._stderr_events_enabled = self._process.config.stderr.events_enabled
5733
5772
 
5734
5773
  _child_log: ta.Optional[logging.Logger] = None # the current logger (normal_log or capture_log)
5735
5774
  _normal_log: ta.Optional[logging.Logger] = None # the "normal" (non-capture) logger
@@ -5800,7 +5839,7 @@ class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5800
5839
  if not data:
5801
5840
  return
5802
5841
 
5803
- if self._process.context.config.strip_ansi:
5842
+ if self._server_config.strip_ansi:
5804
5843
  data = strip_escapes(as_bytes(data))
5805
5844
 
5806
5845
  if self._child_log:
@@ -5898,7 +5937,7 @@ class OutputDispatcherImpl(BaseDispatcherImpl, OutputDispatcher):
5898
5937
  self.close()
5899
5938
 
5900
5939
 
5901
- class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
5940
+ class ProcessInputDispatcherImpl(BaseProcessDispatcherImpl, ProcessInputDispatcher):
5902
5941
  def __init__(
5903
5942
  self,
5904
5943
  process: Process,
@@ -5906,12 +5945,14 @@ class InputDispatcherImpl(BaseDispatcherImpl, InputDispatcher):
5906
5945
  fd: Fd,
5907
5946
  *,
5908
5947
  event_callbacks: EventCallbacks,
5948
+ server_config: ServerConfig,
5909
5949
  ) -> None:
5910
5950
  super().__init__(
5911
5951
  process,
5912
5952
  channel,
5913
5953
  fd,
5914
5954
  event_callbacks=event_callbacks,
5955
+ server_config=server_config,
5915
5956
  )
5916
5957
 
5917
5958
  self._input_buffer = b''
@@ -6071,7 +6112,7 @@ class ProcessGroupImpl(ProcessGroup):
6071
6112
  #
6072
6113
 
6073
6114
  def get_unstopped_processes(self) -> ta.List[Process]:
6074
- return [x for x in self if not x.get_state().stopped]
6115
+ return [x for x in self if not x.state.stopped]
6075
6116
 
6076
6117
  def stop_all(self) -> None:
6077
6118
  processes = list(self._by_name.values())
@@ -6079,7 +6120,7 @@ class ProcessGroupImpl(ProcessGroup):
6079
6120
  processes.reverse() # stop in desc priority order
6080
6121
 
6081
6122
  for proc in processes:
6082
- state = proc.get_state()
6123
+ state = proc.state
6083
6124
  if state == ProcessState.RUNNING:
6084
6125
  # RUNNING -> STOPPING
6085
6126
  proc.stop()
@@ -6362,62 +6403,99 @@ class SupervisorSetupImpl(SupervisorSetup):
6362
6403
 
6363
6404
 
6364
6405
  ########################################
6365
- # ../spawning.py
6366
-
6367
-
6368
- @dc.dataclass(frozen=True)
6369
- class SpawnedProcess:
6370
- pid: Pid
6371
- pipes: ProcessPipes
6372
- dispatchers: Dispatchers
6373
-
6374
-
6375
- class ProcessSpawnError(RuntimeError):
6376
- pass
6377
-
6406
+ # ../io.py
6378
6407
 
6379
- class ProcessSpawning:
6380
- @property
6381
- @abc.abstractmethod
6382
- def process(self) -> Process:
6383
- raise NotImplementedError
6384
6408
 
6385
- #
6409
+ ##
6386
6410
 
6387
- @abc.abstractmethod
6388
- def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
6389
- raise NotImplementedError
6390
6411
 
6412
+ class IoManager:
6413
+ def __init__(
6414
+ self,
6415
+ *,
6416
+ poller: Poller,
6417
+ process_groups: ProcessGroupManager,
6418
+ ) -> None:
6419
+ super().__init__()
6391
6420
 
6392
- ########################################
6393
- # ../supervisor.py
6421
+ self._poller = poller
6422
+ self._process_groups = process_groups
6394
6423
 
6424
+ def get_dispatchers(self) -> Dispatchers:
6425
+ return Dispatchers(
6426
+ d
6427
+ for p in self._process_groups.all_processes()
6428
+ for d in p.get_dispatchers()
6429
+ )
6395
6430
 
6396
- ##
6431
+ def poll(self) -> None:
6432
+ dispatchers = self.get_dispatchers()
6397
6433
 
6434
+ for fd, dispatcher in dispatchers.items():
6435
+ if dispatcher.readable():
6436
+ self._poller.register_readable(fd)
6437
+ if dispatcher.writable():
6438
+ self._poller.register_writable(fd)
6398
6439
 
6399
- class ExitNow(Exception): # noqa
6400
- pass
6440
+ timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
6441
+ r, w = self._poller.poll(timeout)
6401
6442
 
6443
+ for fd in r:
6444
+ if fd in dispatchers:
6445
+ try:
6446
+ dispatcher = dispatchers[fd]
6447
+ log.debug('read event caused by %r', dispatcher)
6448
+ dispatcher.handle_read_event()
6449
+ if not dispatcher.readable():
6450
+ self._poller.unregister_readable(fd)
6451
+ except ExitNow:
6452
+ raise
6453
+ except Exception: # noqa
6454
+ dispatchers[fd].handle_error()
6455
+ else:
6456
+ # if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
6457
+ # time, which may cause 100% cpu usage
6458
+ log.debug('unexpected read event from fd %r', fd)
6459
+ try:
6460
+ self._poller.unregister_readable(fd)
6461
+ except Exception: # noqa
6462
+ pass
6402
6463
 
6403
- def timeslice(period: int, when: float) -> int:
6404
- return int(when - (when % period))
6464
+ for fd in w:
6465
+ if fd in dispatchers:
6466
+ try:
6467
+ dispatcher = dispatchers[fd]
6468
+ log.debug('write event caused by %r', dispatcher)
6469
+ dispatcher.handle_write_event()
6470
+ if not dispatcher.writable():
6471
+ self._poller.unregister_writable(fd)
6472
+ except ExitNow:
6473
+ raise
6474
+ except Exception: # noqa
6475
+ dispatchers[fd].handle_error()
6476
+ else:
6477
+ log.debug('unexpected write event from fd %r', fd)
6478
+ try:
6479
+ self._poller.unregister_writable(fd)
6480
+ except Exception: # noqa
6481
+ pass
6405
6482
 
6406
6483
 
6407
- ##
6484
+ ########################################
6485
+ # ../signals.py
6408
6486
 
6409
6487
 
6410
6488
  class SignalHandler:
6411
6489
  def __init__(
6412
6490
  self,
6413
6491
  *,
6414
- context: ServerContextImpl,
6492
+ states: SupervisorStateManager,
6415
6493
  signal_receiver: SignalReceiver,
6416
6494
  process_groups: ProcessGroupManager,
6417
6495
  ) -> None:
6418
6496
  super().__init__()
6419
6497
 
6420
- self._context = context
6498
+ self._states = states
6421
6499
  self._signal_receiver = signal_receiver
6422
6500
  self._process_groups = process_groups
6423
6501
 
@@ -6438,14 +6516,14 @@ class SignalHandler:
6438
6516
 
6439
6517
  if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
6440
6518
  log.warning('received %s indicating exit request', sig_name(sig))
6441
- self._context.set_state(SupervisorState.SHUTDOWN)
6519
+ self._states.set_state(SupervisorState.SHUTDOWN)
6442
6520
 
6443
6521
  elif sig == signal.SIGHUP:
6444
- if self._context.state == SupervisorState.SHUTDOWN:
6522
+ if self._states.state == SupervisorState.SHUTDOWN:
6445
6523
  log.warning('ignored %s indicating restart request (shutdown in progress)', sig_name(sig)) # noqa
6446
6524
  else:
6447
6525
  log.warning('received %s indicating restart request', sig_name(sig)) # noqa
6448
- self._context.set_state(SupervisorState.RESTARTING)
6526
+ self._states.set_state(SupervisorState.RESTARTING)
6449
6527
 
6450
6528
  elif sig == signal.SIGCHLD:
6451
6529
  log.debug('received %s indicating a child quit', sig_name(sig))
@@ -6455,282 +6533,39 @@ class SignalHandler:
6455
6533
 
6456
6534
  for p in self._process_groups.all_processes():
6457
6535
  for d in p.get_dispatchers():
6458
- if isinstance(d, OutputDispatcher):
6536
+ if isinstance(d, ProcessOutputDispatcher):
6459
6537
  d.reopen_logs()
6460
6538
 
6461
6539
  else:
6462
6540
  log.debug('received %s indicating nothing', sig_name(sig))
6463
6541
 
6464
6542
 
6465
- ##
6543
+ ########################################
6544
+ # ../spawning.py
6466
6545
 
6467
6546
 
6468
- class ProcessGroupFactory(Func1[ProcessGroupConfig, ProcessGroup]):
6469
- pass
6547
+ @dc.dataclass(frozen=True)
6548
+ class SpawnedProcess:
6549
+ pid: Pid
6550
+ pipes: ProcessPipes
6551
+ dispatchers: Dispatchers
6470
6552
 
6471
6553
 
6472
- class Supervisor:
6473
- def __init__(
6474
- self,
6475
- *,
6476
- context: ServerContextImpl,
6477
- poller: Poller,
6478
- process_groups: ProcessGroupManager,
6479
- signal_handler: SignalHandler,
6480
- event_callbacks: EventCallbacks,
6481
- process_group_factory: ProcessGroupFactory,
6482
- pid_history: PidHistory,
6483
- setup: SupervisorSetup,
6484
- ) -> None:
6485
- super().__init__()
6554
+ class ProcessSpawnError(RuntimeError):
6555
+ pass
6486
6556
 
6487
- self._context = context
6488
- self._poller = poller
6489
- self._process_groups = process_groups
6490
- self._signal_handler = signal_handler
6491
- self._event_callbacks = event_callbacks
6492
- self._process_group_factory = process_group_factory
6493
- self._pid_history = pid_history
6494
- self._setup = setup
6495
-
6496
- self._ticks: ta.Dict[int, float] = {}
6497
- self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
6498
- self._stopping = False # set after we detect that we are handling a stop request
6499
- self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
6500
-
6501
- #
6502
6557
 
6558
+ class ProcessSpawning:
6503
6559
  @property
6504
- def context(self) -> ServerContextImpl:
6505
- return self._context
6506
-
6507
- def get_state(self) -> SupervisorState:
6508
- return self._context.state
6509
-
6510
- #
6511
-
6512
- def add_process_group(self, config: ProcessGroupConfig) -> bool:
6513
- if self._process_groups.get(config.name) is not None:
6514
- return False
6515
-
6516
- group = check_isinstance(self._process_group_factory(config), ProcessGroup)
6517
- for process in group:
6518
- process.after_setuid()
6519
-
6520
- self._process_groups.add(group)
6521
-
6522
- return True
6523
-
6524
- def remove_process_group(self, name: str) -> bool:
6525
- if self._process_groups[name].get_unstopped_processes():
6526
- return False
6527
-
6528
- self._process_groups.remove(name)
6529
-
6530
- return True
6531
-
6532
- #
6533
-
6534
- def shutdown_report(self) -> ta.List[Process]:
6535
- unstopped: ta.List[Process] = []
6536
-
6537
- for group in self._process_groups:
6538
- unstopped.extend(group.get_unstopped_processes())
6539
-
6540
- if unstopped:
6541
- # throttle 'waiting for x to die' reports
6542
- now = time.time()
6543
- if now > (self._last_shutdown_report + 3): # every 3 secs
6544
- names = [p.config.name for p in unstopped]
6545
- namestr = ', '.join(names)
6546
- log.info('waiting for %s to die', namestr)
6547
- self._last_shutdown_report = now
6548
- for proc in unstopped:
6549
- log.debug('%s state: %s', proc.config.name, proc.get_state().name)
6550
-
6551
- return unstopped
6552
-
6553
- #
6554
-
6555
- def main(self, **kwargs: ta.Any) -> None:
6556
- self._setup.setup()
6557
- try:
6558
- self.run(**kwargs)
6559
- finally:
6560
- self._setup.cleanup()
6561
-
6562
- def run(
6563
- self,
6564
- *,
6565
- callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
6566
- ) -> None:
6567
- self._process_groups.clear()
6568
- self._stop_groups = None # clear
6569
-
6570
- self._event_callbacks.clear()
6571
-
6572
- try:
6573
- for config in self._context.config.groups or []:
6574
- self.add_process_group(config)
6575
-
6576
- self._signal_handler.set_signals()
6577
-
6578
- self._event_callbacks.notify(SupervisorRunningEvent())
6579
-
6580
- while True:
6581
- if callback is not None and not callback(self):
6582
- break
6583
-
6584
- self._run_once()
6585
-
6586
- finally:
6587
- self._poller.close()
6560
+ @abc.abstractmethod
6561
+ def process(self) -> Process:
6562
+ raise NotImplementedError
6588
6563
 
6589
6564
  #
6590
6565
 
6591
- def _run_once(self) -> None:
6592
- self._poll()
6593
- self._reap()
6594
- self._signal_handler.handle_signals()
6595
- self._tick()
6596
-
6597
- if self._context.state < SupervisorState.RUNNING:
6598
- self._ordered_stop_groups_phase_2()
6599
-
6600
- def _ordered_stop_groups_phase_1(self) -> None:
6601
- if self._stop_groups:
6602
- # stop the last group (the one with the "highest" priority)
6603
- self._stop_groups[-1].stop_all()
6604
-
6605
- def _ordered_stop_groups_phase_2(self) -> None:
6606
- # after phase 1 we've transitioned and reaped, let's see if we can remove the group we stopped from the
6607
- # stop_groups queue.
6608
- if self._stop_groups:
6609
- # pop the last group (the one with the "highest" priority)
6610
- group = self._stop_groups.pop()
6611
- if group.get_unstopped_processes():
6612
- # if any processes in the group aren't yet in a stopped state, we're not yet done shutting this group
6613
- # down, so push it back on to the end of the stop group queue
6614
- self._stop_groups.append(group)
6615
-
6616
- def get_dispatchers(self) -> Dispatchers:
6617
- return Dispatchers(
6618
- d
6619
- for p in self._process_groups.all_processes()
6620
- for d in p.get_dispatchers()
6621
- )
6622
-
6623
- def _poll(self) -> None:
6624
- dispatchers = self.get_dispatchers()
6625
-
6626
- sorted_groups = list(self._process_groups)
6627
- sorted_groups.sort()
6628
-
6629
- if self._context.state < SupervisorState.RUNNING:
6630
- if not self._stopping:
6631
- # first time, set the stopping flag, do a notification and set stop_groups
6632
- self._stopping = True
6633
- self._stop_groups = sorted_groups[:]
6634
- self._event_callbacks.notify(SupervisorStoppingEvent())
6635
-
6636
- self._ordered_stop_groups_phase_1()
6637
-
6638
- if not self.shutdown_report():
6639
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
6640
- raise ExitNow
6641
-
6642
- for fd, dispatcher in dispatchers.items():
6643
- if dispatcher.readable():
6644
- self._poller.register_readable(fd)
6645
- if dispatcher.writable():
6646
- self._poller.register_writable(fd)
6647
-
6648
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
6649
- r, w = self._poller.poll(timeout)
6650
-
6651
- for fd in r:
6652
- if fd in dispatchers:
6653
- try:
6654
- dispatcher = dispatchers[fd]
6655
- log.debug('read event caused by %r', dispatcher)
6656
- dispatcher.handle_read_event()
6657
- if not dispatcher.readable():
6658
- self._poller.unregister_readable(fd)
6659
- except ExitNow:
6660
- raise
6661
- except Exception: # noqa
6662
- dispatchers[fd].handle_error()
6663
- else:
6664
- # if the fd is not in combined map, we should unregister it. otherwise, it will be polled every
6665
- # time, which may cause 100% cpu usage
6666
- log.debug('unexpected read event from fd %r', fd)
6667
- try:
6668
- self._poller.unregister_readable(fd)
6669
- except Exception: # noqa
6670
- pass
6671
-
6672
- for fd in w:
6673
- if fd in dispatchers:
6674
- try:
6675
- dispatcher = dispatchers[fd]
6676
- log.debug('write event caused by %r', dispatcher)
6677
- dispatcher.handle_write_event()
6678
- if not dispatcher.writable():
6679
- self._poller.unregister_writable(fd)
6680
- except ExitNow:
6681
- raise
6682
- except Exception: # noqa
6683
- dispatchers[fd].handle_error()
6684
- else:
6685
- log.debug('unexpected write event from fd %r', fd)
6686
- try:
6687
- self._poller.unregister_writable(fd)
6688
- except Exception: # noqa
6689
- pass
6690
-
6691
- for group in sorted_groups:
6692
- for process in group:
6693
- process.transition()
6694
-
6695
- def _reap(self, *, once: bool = False, depth: int = 0) -> None:
6696
- if depth >= 100:
6697
- return
6698
-
6699
- pid, sts = self._context.waitpid()
6700
- if not pid:
6701
- return
6702
-
6703
- process = self._pid_history.get(pid, None)
6704
- if process is None:
6705
- _, msg = decode_wait_status(check_not_none(sts))
6706
- log.info('reaped unknown pid %s (%s)', pid, msg)
6707
- else:
6708
- process.finish(check_not_none(sts))
6709
- del self._pid_history[pid]
6710
-
6711
- if not once:
6712
- # keep reaping until no more kids to reap, but don't recurse infinitely
6713
- self._reap(once=False, depth=depth + 1)
6714
-
6715
- def _tick(self, now: ta.Optional[float] = None) -> None:
6716
- """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
6717
-
6718
- if now is None:
6719
- # now won't be None in unit tests
6720
- now = time.time()
6721
-
6722
- for event in TICK_EVENTS:
6723
- period = event.period
6724
-
6725
- last_tick = self._ticks.get(period)
6726
- if last_tick is None:
6727
- # we just started up
6728
- last_tick = self._ticks[period] = timeslice(period, now)
6729
-
6730
- this_tick = timeslice(period, now)
6731
- if this_tick != last_tick:
6732
- self._ticks[period] = this_tick
6733
- self._event_callbacks.notify(event(this_tick, self))
6566
+ @abc.abstractmethod
6567
+ def spawn(self) -> SpawnedProcess: # Raises[ProcessSpawnError]
6568
+ raise NotImplementedError
6734
6569
 
6735
6570
 
6736
6571
  ########################################
@@ -6752,7 +6587,7 @@ class ProcessImpl(Process):
6752
6587
  config: ProcessConfig,
6753
6588
  group: ProcessGroup,
6754
6589
  *,
6755
- context: ServerContext,
6590
+ supervisor_states: SupervisorStateManager,
6756
6591
  event_callbacks: EventCallbacks,
6757
6592
  process_spawning_factory: ProcessSpawningFactory,
6758
6593
  ) -> None:
@@ -6761,7 +6596,7 @@ class ProcessImpl(Process):
6761
6596
  self._config = config
6762
6597
  self._group = group
6763
6598
 
6764
- self._context = context
6599
+ self._supervisor_states = supervisor_states
6765
6600
  self._event_callbacks = event_callbacks
6766
6601
 
6767
6602
  self._spawning = process_spawning_factory(self)
@@ -6792,7 +6627,7 @@ class ProcessImpl(Process):
6792
6627
  #
6793
6628
 
6794
6629
  def __repr__(self) -> str:
6795
- return f'<Subprocess at {id(self)} with name {self._config.name} in state {self.get_state().name}>'
6630
+ return f'<Subprocess at {id(self)} with name {self._config.name} in state {self._state.name}>'
6796
6631
 
6797
6632
  #
6798
6633
 
@@ -6814,10 +6649,6 @@ class ProcessImpl(Process):
6814
6649
 
6815
6650
  #
6816
6651
 
6817
- @property
6818
- def context(self) -> ServerContext:
6819
- return self._context
6820
-
6821
6652
  @property
6822
6653
  def state(self) -> ProcessState:
6823
6654
  return self._state
@@ -6880,7 +6711,7 @@ class ProcessImpl(Process):
6880
6711
  if stdin_fd is None:
6881
6712
  raise OSError(errno.EPIPE, 'Process has no stdin channel')
6882
6713
 
6883
- dispatcher = check_isinstance(self._dispatchers[stdin_fd], InputDispatcher)
6714
+ dispatcher = check_isinstance(self._dispatchers[stdin_fd], ProcessInputDispatcher)
6884
6715
  if dispatcher.closed:
6885
6716
  raise OSError(errno.EPIPE, "Process' stdin channel is closed")
6886
6717
 
@@ -7150,9 +6981,6 @@ class ProcessImpl(Process):
7150
6981
  self._pipes = ProcessPipes()
7151
6982
  self._dispatchers = Dispatchers([])
7152
6983
 
7153
- def get_state(self) -> ProcessState:
7154
- return self._state
7155
-
7156
6984
  def transition(self) -> None:
7157
6985
  now = time.time()
7158
6986
  state = self._state
@@ -7161,7 +6989,7 @@ class ProcessImpl(Process):
7161
6989
 
7162
6990
  logger = log
7163
6991
 
7164
- if self.context.state > SupervisorState.RESTARTING:
6992
+ if self._supervisor_states.state > SupervisorState.RESTARTING:
7165
6993
  # dont start any processes if supervisor is shutting down
7166
6994
  if state == ProcessState.EXITED:
7167
6995
  if self._config.autorestart:
@@ -7225,11 +7053,11 @@ class ProcessImpl(Process):
7225
7053
  # ../spawningimpl.py
7226
7054
 
7227
7055
 
7228
- class OutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, OutputDispatcher]):
7056
+ class ProcessOutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, ProcessOutputDispatcher]):
7229
7057
  pass
7230
7058
 
7231
7059
 
7232
- class InputDispatcherFactory(Func3[Process, str, Fd, InputDispatcher]):
7060
+ class ProcessInputDispatcherFactory(Func3[Process, str, Fd, ProcessInputDispatcher]):
7233
7061
  pass
7234
7062
 
7235
7063
 
@@ -7247,8 +7075,8 @@ class ProcessSpawningImpl(ProcessSpawning):
7247
7075
  server_config: ServerConfig,
7248
7076
  pid_history: PidHistory,
7249
7077
 
7250
- output_dispatcher_factory: OutputDispatcherFactory,
7251
- input_dispatcher_factory: InputDispatcherFactory,
7078
+ output_dispatcher_factory: ProcessOutputDispatcherFactory,
7079
+ input_dispatcher_factory: ProcessInputDispatcherFactory,
7252
7080
 
7253
7081
  inherited_fds: ta.Optional[InheritedFds] = None,
7254
7082
  ) -> None:
@@ -7388,21 +7216,21 @@ class ProcessSpawningImpl(ProcessSpawning):
7388
7216
  self.process,
7389
7217
  ProcessCommunicationStdoutEvent,
7390
7218
  pipes.stdout,
7391
- ), OutputDispatcher))
7219
+ ), ProcessOutputDispatcher))
7392
7220
 
7393
7221
  if pipes.stderr is not None:
7394
7222
  dispatchers.append(check_isinstance(self._output_dispatcher_factory(
7395
7223
  self.process,
7396
7224
  ProcessCommunicationStderrEvent,
7397
7225
  pipes.stderr,
7398
- ), OutputDispatcher))
7226
+ ), ProcessOutputDispatcher))
7399
7227
 
7400
7228
  if pipes.stdin is not None:
7401
7229
  dispatchers.append(check_isinstance(self._input_dispatcher_factory(
7402
7230
  self.process,
7403
7231
  'stdin',
7404
7232
  pipes.stdin,
7405
- ), InputDispatcher))
7233
+ ), ProcessInputDispatcher))
7406
7234
 
7407
7235
  return Dispatchers(dispatchers)
7408
7236
 
@@ -7531,6 +7359,278 @@ def check_execv_args(
7531
7359
  raise NoPermissionError(f'No permission to run command {exe!r}')
7532
7360
 
7533
7361
 
7362
+ ########################################
7363
+ # ../supervisor.py
7364
+
7365
+
7366
+ ##
7367
+
7368
+
7369
+ def timeslice(period: int, when: float) -> int:
7370
+ return int(when - (when % period))
7371
+
7372
+
7373
+ ##
7374
+
7375
+
7376
+ class SupervisorStateManagerImpl(SupervisorStateManager):
7377
+ def __init__(self) -> None:
7378
+ super().__init__()
7379
+
7380
+ self._state: SupervisorState = SupervisorState.RUNNING
7381
+
7382
+ @property
7383
+ def state(self) -> SupervisorState:
7384
+ return self._state
7385
+
7386
+ def set_state(self, state: SupervisorState) -> None:
7387
+ self._state = state
7388
+
7389
+
7390
+ ##
7391
+
7392
+
7393
+ class ProcessGroupFactory(Func1[ProcessGroupConfig, ProcessGroup]):
7394
+ pass
7395
+
7396
+
7397
+ class Supervisor:
7398
+ def __init__(
7399
+ self,
7400
+ *,
7401
+ config: ServerConfig,
7402
+ poller: Poller,
7403
+ process_groups: ProcessGroupManager,
7404
+ signal_handler: SignalHandler,
7405
+ event_callbacks: EventCallbacks,
7406
+ process_group_factory: ProcessGroupFactory,
7407
+ pid_history: PidHistory,
7408
+ setup: SupervisorSetup,
7409
+ states: SupervisorStateManager,
7410
+ io: IoManager,
7411
+ ) -> None:
7412
+ super().__init__()
7413
+
7414
+ self._config = config
7415
+ self._poller = poller
7416
+ self._process_groups = process_groups
7417
+ self._signal_handler = signal_handler
7418
+ self._event_callbacks = event_callbacks
7419
+ self._process_group_factory = process_group_factory
7420
+ self._pid_history = pid_history
7421
+ self._setup = setup
7422
+ self._states = states
7423
+ self._io = io
7424
+
7425
+ self._ticks: ta.Dict[int, float] = {}
7426
+ self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
7427
+ self._stopping = False # set after we detect that we are handling a stop request
7428
+ self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
7429
+
7430
+ #
7431
+
7432
+ @property
7433
+ def state(self) -> SupervisorState:
7434
+ return self._states.state
7435
+
7436
+ #
7437
+
7438
+ def add_process_group(self, config: ProcessGroupConfig) -> bool:
7439
+ if self._process_groups.get(config.name) is not None:
7440
+ return False
7441
+
7442
+ group = check_isinstance(self._process_group_factory(config), ProcessGroup)
7443
+ for process in group:
7444
+ process.after_setuid()
7445
+
7446
+ self._process_groups.add(group)
7447
+
7448
+ return True
7449
+
7450
+ def remove_process_group(self, name: str) -> bool:
7451
+ if self._process_groups[name].get_unstopped_processes():
7452
+ return False
7453
+
7454
+ self._process_groups.remove(name)
7455
+
7456
+ return True
7457
+
7458
+ #
7459
+
7460
+ def shutdown_report(self) -> ta.List[Process]:
7461
+ unstopped: ta.List[Process] = []
7462
+
7463
+ for group in self._process_groups:
7464
+ unstopped.extend(group.get_unstopped_processes())
7465
+
7466
+ if unstopped:
7467
+ # throttle 'waiting for x to die' reports
7468
+ now = time.time()
7469
+ if now > (self._last_shutdown_report + 3): # every 3 secs
7470
+ names = [p.config.name for p in unstopped]
7471
+ namestr = ', '.join(names)
7472
+ log.info('waiting for %s to die', namestr)
7473
+ self._last_shutdown_report = now
7474
+ for proc in unstopped:
7475
+ log.debug('%s state: %s', proc.config.name, proc.state.name)
7476
+
7477
+ return unstopped
7478
+
7479
+ #
7480
+
7481
+ def main(self, **kwargs: ta.Any) -> None:
7482
+ self._setup.setup()
7483
+ try:
7484
+ self.run(**kwargs)
7485
+ finally:
7486
+ self._setup.cleanup()
7487
+
7488
+ def run(
7489
+ self,
7490
+ *,
7491
+ callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
7492
+ ) -> None:
7493
+ self._process_groups.clear()
7494
+ self._stop_groups = None # clear
7495
+
7496
+ self._event_callbacks.clear()
7497
+
7498
+ try:
7499
+ for config in self._config.groups or []:
7500
+ self.add_process_group(config)
7501
+
7502
+ self._signal_handler.set_signals()
7503
+
7504
+ self._event_callbacks.notify(SupervisorRunningEvent())
7505
+
7506
+ while True:
7507
+ if callback is not None and not callback(self):
7508
+ break
7509
+
7510
+ self._run_once()
7511
+
7512
+ finally:
7513
+ self._poller.close()
7514
+
7515
+ #
7516
+
7517
+ def _run_once(self) -> None:
7518
+ self._poll()
7519
+ self._reap()
7520
+ self._signal_handler.handle_signals()
7521
+ self._tick()
7522
+
7523
+ if self._states.state < SupervisorState.RUNNING:
7524
+ self._ordered_stop_groups_phase_2()
7525
+
7526
+ def _ordered_stop_groups_phase_1(self) -> None:
7527
+ if self._stop_groups:
7528
+ # stop the last group (the one with the "highest" priority)
7529
+ self._stop_groups[-1].stop_all()
7530
+
7531
+ def _ordered_stop_groups_phase_2(self) -> None:
7532
+ # after phase 1 we've transitioned and reaped, let's see if we can remove the group we stopped from the
7533
+ # stop_groups queue.
7534
+ if self._stop_groups:
7535
+ # pop the last group (the one with the "highest" priority)
7536
+ group = self._stop_groups.pop()
7537
+ if group.get_unstopped_processes():
7538
+ # if any processes in the group aren't yet in a stopped state, we're not yet done shutting this group
7539
+ # down, so push it back on to the end of the stop group queue
7540
+ self._stop_groups.append(group)
7541
+
7542
+ def _poll(self) -> None:
7543
+ sorted_groups = list(self._process_groups)
7544
+ sorted_groups.sort()
7545
+
7546
+ if self._states.state < SupervisorState.RUNNING:
7547
+ if not self._stopping:
7548
+ # first time, set the stopping flag, do a notification and set stop_groups
7549
+ self._stopping = True
7550
+ self._stop_groups = sorted_groups[:]
7551
+ self._event_callbacks.notify(SupervisorStoppingEvent())
7552
+
7553
+ self._ordered_stop_groups_phase_1()
7554
+
7555
+ if not self.shutdown_report():
7556
+ # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
7557
+ raise ExitNow
7558
+
7559
+ self._io.poll()
7560
+
7561
+ for group in sorted_groups:
7562
+ for process in group:
7563
+ process.transition()
7564
+
7565
+ def _reap(self, *, once: bool = False, depth: int = 0) -> None:
7566
+ if depth >= 100:
7567
+ return
7568
+
7569
+ wp = waitpid()
7570
+ if wp is None or not wp.pid:
7571
+ return
7572
+
7573
+ process = self._pid_history.get(wp.pid, None)
7574
+ if process is None:
7575
+ _, msg = decode_wait_status(wp.sts)
7576
+ log.info('reaped unknown pid %s (%s)', wp.pid, msg)
7577
+ else:
7578
+ process.finish(wp.sts)
7579
+ del self._pid_history[wp.pid]
7580
+
7581
+ if not once:
7582
+ # keep reaping until no more kids to reap, but don't recurse infinitely
7583
+ self._reap(once=False, depth=depth + 1)
7584
+
7585
+ def _tick(self, now: ta.Optional[float] = None) -> None:
7586
+ """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
7587
+
7588
+ if now is None:
7589
+ # now won't be None in unit tests
7590
+ now = time.time()
7591
+
7592
+ for event in TICK_EVENTS:
7593
+ period = event.period
7594
+
7595
+ last_tick = self._ticks.get(period)
7596
+ if last_tick is None:
7597
+ # we just started up
7598
+ last_tick = self._ticks[period] = timeslice(period, now)
7599
+
7600
+ this_tick = timeslice(period, now)
7601
+ if this_tick != last_tick:
7602
+ self._ticks[period] = this_tick
7603
+ self._event_callbacks.notify(event(this_tick, self))
7604
+
7605
+
7606
+ ##
7607
+
7608
+
7609
+ class WaitedPid(ta.NamedTuple):
7610
+ pid: Pid
7611
+ sts: Rc
7612
+
7613
+
7614
+ def waitpid() -> ta.Optional[WaitedPid]:
7615
+ # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
7616
+ # still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
7617
+ # waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
7618
+ # normal course in the mainloop), we'll eventually reap the child that we tried to reap during the interrupted
7619
+ # call. At least on Linux, this appears to be true, or at least stopping 50 processes at once never left zombies
7620
+ # lying around.
7621
+ try:
7622
+ pid, sts = os.waitpid(-1, os.WNOHANG)
7623
+ except OSError as exc:
7624
+ code = exc.args[0]
7625
+ if code not in (errno.ECHILD, errno.EINTR):
7626
+ log.critical('waitpid error %r; a process may not be cleaned up properly', code)
7627
+ if code == errno.EINTR:
7628
+ log.debug('EINTR during reap')
7629
+ return None
7630
+ else:
7631
+ return WaitedPid(pid, sts) # type: ignore
7632
+
7633
+
7534
7634
  ########################################
7535
7635
  # ../inject.py
7536
7636
 
@@ -7551,17 +7651,21 @@ def bind_server(
7551
7651
 
7552
7652
  inj.bind(DaemonizeListener, array=True, to_key=Poller),
7553
7653
 
7554
- inj.bind(ServerContextImpl, singleton=True),
7555
- inj.bind(ServerContext, to_key=ServerContextImpl),
7556
-
7557
7654
  inj.bind(EventCallbacks, singleton=True),
7558
7655
 
7559
7656
  inj.bind(SignalReceiver, singleton=True),
7560
7657
 
7658
+ inj.bind(IoManager, singleton=True),
7659
+
7561
7660
  inj.bind(SignalHandler, singleton=True),
7661
+
7562
7662
  inj.bind(ProcessGroupManager, singleton=True),
7663
+
7563
7664
  inj.bind(Supervisor, singleton=True),
7564
7665
 
7666
+ inj.bind(SupervisorStateManagerImpl, singleton=True),
7667
+ inj.bind(SupervisorStateManager, to_key=SupervisorStateManagerImpl),
7668
+
7565
7669
  inj.bind(PidHistory()),
7566
7670
 
7567
7671
  inj.bind_factory(ProcessGroupImpl, ProcessGroupFactory),
@@ -7569,8 +7673,8 @@ def bind_server(
7569
7673
 
7570
7674
  inj.bind_factory(ProcessSpawningImpl, ProcessSpawningFactory),
7571
7675
 
7572
- inj.bind_factory(OutputDispatcherImpl, OutputDispatcherFactory),
7573
- inj.bind_factory(InputDispatcherImpl, InputDispatcherFactory),
7676
+ inj.bind_factory(ProcessOutputDispatcherImpl, ProcessOutputDispatcherFactory),
7677
+ inj.bind_factory(ProcessInputDispatcherImpl, ProcessInputDispatcherFactory),
7574
7678
  ]
7575
7679
 
7576
7680
  #
@@ -7627,7 +7731,7 @@ def main(
7627
7731
  if not no_logging:
7628
7732
  configure_standard_logging(
7629
7733
  'INFO',
7630
- handler_factory=journald_log_handler_factory if not args.no_journald else None,
7734
+ handler_factory=journald_log_handler_factory if not (args.no_journald or is_debugger_attached()) else None,
7631
7735
  )
7632
7736
 
7633
7737
  #
@@ -7650,7 +7754,6 @@ def main(
7650
7754
  inherited_fds=inherited_fds,
7651
7755
  ))
7652
7756
 
7653
- context = injector[ServerContextImpl]
7654
7757
  supervisor = injector[Supervisor]
7655
7758
 
7656
7759
  try:
@@ -7658,7 +7761,7 @@ def main(
7658
7761
  except ExitNow:
7659
7762
  pass
7660
7763
 
7661
- if context.state < SupervisorState.RESTARTING:
7764
+ if supervisor.state < SupervisorState.RESTARTING:
7662
7765
  break
7663
7766
 
7664
7767