ominfra 0.0.0.dev128__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.
@@ -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