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.
- ominfra/deploy/_executor.py +24 -0
- ominfra/pyremote/_runcommands.py +24 -0
- ominfra/scripts/journald2aws.py +24 -0
- ominfra/scripts/supervisor.py +543 -440
- ominfra/supervisor/dispatchers.py +3 -3
- ominfra/supervisor/dispatchersimpl.py +17 -11
- ominfra/supervisor/groupsimpl.py +2 -2
- ominfra/supervisor/inject.py +17 -12
- ominfra/supervisor/io.py +82 -0
- ominfra/supervisor/main.py +5 -6
- ominfra/supervisor/processimpl.py +7 -14
- ominfra/supervisor/signals.py +66 -0
- ominfra/supervisor/spawningimpl.py +9 -9
- ominfra/supervisor/supervisor.py +65 -135
- ominfra/supervisor/types.py +38 -25
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/RECORD +21 -20
- ominfra/supervisor/context.py +0 -80
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev128.dist-info → ominfra-0.0.0.dev129.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -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(
|
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
|
-
|
3061
|
-
|
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
|
-
|
3064
|
-
|
3065
|
-
|
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
|
-
|
3068
|
-
|
3069
|
-
|
3070
|
-
|
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
|
-
|
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
|
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
|
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
|
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(
|
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
|
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,
|
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,
|
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
|
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
|
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 =
|
5768
|
+
self._log_to_main_log = self._server_config.loglevel <= self._main_log_level
|
5729
5769
|
|
5730
|
-
|
5731
|
-
self.
|
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.
|
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
|
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.
|
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.
|
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
|
-
# ../
|
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
|
-
|
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
|
-
|
6400
|
-
|
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
|
-
|
6404
|
-
|
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
|
-
|
6492
|
+
states: SupervisorStateManager,
|
6415
6493
|
signal_receiver: SignalReceiver,
|
6416
6494
|
process_groups: ProcessGroupManager,
|
6417
6495
|
) -> None:
|
6418
6496
|
super().__init__()
|
6419
6497
|
|
6420
|
-
self.
|
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.
|
6519
|
+
self._states.set_state(SupervisorState.SHUTDOWN)
|
6442
6520
|
|
6443
6521
|
elif sig == signal.SIGHUP:
|
6444
|
-
if self.
|
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.
|
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,
|
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
|
-
|
6469
|
-
|
6547
|
+
@dc.dataclass(frozen=True)
|
6548
|
+
class SpawnedProcess:
|
6549
|
+
pid: Pid
|
6550
|
+
pipes: ProcessPipes
|
6551
|
+
dispatchers: Dispatchers
|
6470
6552
|
|
6471
6553
|
|
6472
|
-
class
|
6473
|
-
|
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
|
-
|
6505
|
-
|
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
|
-
|
6592
|
-
|
6593
|
-
|
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
|
-
|
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.
|
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.
|
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],
|
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.
|
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
|
7056
|
+
class ProcessOutputDispatcherFactory(Func3[Process, ta.Type[ProcessCommunicationEvent], Fd, ProcessOutputDispatcher]):
|
7229
7057
|
pass
|
7230
7058
|
|
7231
7059
|
|
7232
|
-
class
|
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:
|
7251
|
-
input_dispatcher_factory:
|
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
|
-
),
|
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
|
-
),
|
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
|
-
),
|
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(
|
7573
|
-
inj.bind_factory(
|
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
|
7764
|
+
if supervisor.state < SupervisorState.RESTARTING:
|
7662
7765
|
break
|
7663
7766
|
|
7664
7767
|
|