ominfra 0.0.0.dev119__py3-none-any.whl → 0.0.0.dev120__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/scripts/supervisor.py +108 -99
- ominfra/supervisor/dispatchers.py +4 -4
- ominfra/supervisor/events.py +4 -7
- ominfra/supervisor/process.py +8 -7
- ominfra/supervisor/supervisor.py +95 -86
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/RECORD +11 -11
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev119.dist-info → ominfra-0.0.0.dev120.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -22,6 +22,7 @@ import itertools
|
|
22
22
|
import json
|
23
23
|
import logging
|
24
24
|
import os
|
25
|
+
import os.path
|
25
26
|
import pwd
|
26
27
|
import re
|
27
28
|
import resource
|
@@ -1602,12 +1603,9 @@ class EventCallbacks:
|
|
1602
1603
|
|
1603
1604
|
EVENT_CALLBACKS = EventCallbacks()
|
1604
1605
|
|
1605
|
-
notify_event = EVENT_CALLBACKS.notify
|
1606
|
-
clear_events = EVENT_CALLBACKS.clear
|
1607
|
-
|
1608
1606
|
|
1609
1607
|
class Event(abc.ABC): # noqa
|
1610
|
-
"""Abstract event type
|
1608
|
+
"""Abstract event type."""
|
1611
1609
|
|
1612
1610
|
|
1613
1611
|
class ProcessLogEvent(Event, abc.ABC):
|
@@ -1687,7 +1685,7 @@ class RemoteCommunicationEvent(Event):
|
|
1687
1685
|
|
1688
1686
|
|
1689
1687
|
class SupervisorStateChangeEvent(Event):
|
1690
|
-
"""
|
1688
|
+
"""Abstract class."""
|
1691
1689
|
|
1692
1690
|
def payload(self):
|
1693
1691
|
return ''
|
@@ -1709,7 +1707,7 @@ class EventRejectedEvent: # purposely does not subclass Event
|
|
1709
1707
|
|
1710
1708
|
|
1711
1709
|
class ProcessStateEvent(Event):
|
1712
|
-
"""
|
1710
|
+
"""Abstract class, never raised directly."""
|
1713
1711
|
frm = None
|
1714
1712
|
to = None
|
1715
1713
|
|
@@ -1798,7 +1796,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent):
|
|
1798
1796
|
|
1799
1797
|
|
1800
1798
|
class TickEvent(Event):
|
1801
|
-
"""
|
1799
|
+
"""Abstract."""
|
1802
1800
|
|
1803
1801
|
def __init__(self, when, supervisord):
|
1804
1802
|
super().__init__()
|
@@ -2752,10 +2750,10 @@ class OutputDispatcher(Dispatcher):
|
|
2752
2750
|
|
2753
2751
|
if self._channel == 'stdout':
|
2754
2752
|
if self._stdout_events_enabled:
|
2755
|
-
|
2753
|
+
EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
2756
2754
|
|
2757
2755
|
elif self._stderr_events_enabled:
|
2758
|
-
|
2756
|
+
EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
2759
2757
|
|
2760
2758
|
def record_output(self):
|
2761
2759
|
if self._capture_log is None:
|
@@ -2806,7 +2804,7 @@ class OutputDispatcher(Dispatcher):
|
|
2806
2804
|
channel = self._channel
|
2807
2805
|
procname = self._process.config.name
|
2808
2806
|
event = self.event_type(self._process, self._process.pid, data)
|
2809
|
-
|
2807
|
+
EVENT_CALLBACKS.notify(event)
|
2810
2808
|
|
2811
2809
|
log.debug('%r %s emitted a comm event', procname, channel)
|
2812
2810
|
for handler in self._capture_log.handlers:
|
@@ -3019,7 +3017,7 @@ class Subprocess(AbstractSubprocess):
|
|
3019
3017
|
event_class = self.event_map.get(new_state)
|
3020
3018
|
if event_class is not None:
|
3021
3019
|
event = event_class(self, old_state, expected)
|
3022
|
-
|
3020
|
+
EVENT_CALLBACKS.notify(event)
|
3023
3021
|
|
3024
3022
|
return True
|
3025
3023
|
|
@@ -3136,6 +3134,7 @@ class Subprocess(AbstractSubprocess):
|
|
3136
3134
|
os.dup2(self._pipes['child_stdout'], 2)
|
3137
3135
|
else:
|
3138
3136
|
os.dup2(self._pipes['child_stderr'], 2)
|
3137
|
+
# FIXME: leave debugger fds
|
3139
3138
|
for i in range(3, self.context.config.minfds):
|
3140
3139
|
close_fd(i)
|
3141
3140
|
|
@@ -3171,7 +3170,7 @@ class Subprocess(AbstractSubprocess):
|
|
3171
3170
|
cwd = self.config.directory
|
3172
3171
|
try:
|
3173
3172
|
if cwd is not None:
|
3174
|
-
os.chdir(cwd)
|
3173
|
+
os.chdir(os.path.expanduser(cwd))
|
3175
3174
|
except OSError as why:
|
3176
3175
|
code = errno.errorcode.get(why.args[0], why.args[0])
|
3177
3176
|
msg = f"couldn't chdir to {cwd}: {code}\n"
|
@@ -3227,7 +3226,7 @@ class Subprocess(AbstractSubprocess):
|
|
3227
3226
|
return self.kill(self.config.stopsignal)
|
3228
3227
|
|
3229
3228
|
def stop_report(self) -> None:
|
3230
|
-
"""
|
3229
|
+
"""Log a 'waiting for x to stop' message with throttling."""
|
3231
3230
|
if self.state == ProcessStates.STOPPING:
|
3232
3231
|
now = time.time()
|
3233
3232
|
|
@@ -3357,7 +3356,7 @@ class Subprocess(AbstractSubprocess):
|
|
3357
3356
|
return None
|
3358
3357
|
|
3359
3358
|
def finish(self, sts: int) -> None:
|
3360
|
-
"""
|
3359
|
+
"""The process was reaped and we need to report and manage its state."""
|
3361
3360
|
|
3362
3361
|
self.drain()
|
3363
3362
|
|
@@ -3438,7 +3437,7 @@ class Subprocess(AbstractSubprocess):
|
|
3438
3437
|
# system that this event was rejected so it can be processed again.
|
3439
3438
|
if self.event is not None:
|
3440
3439
|
# Note: this should only be true if we were in the BUSY state when finish() was called.
|
3441
|
-
|
3440
|
+
EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
|
3442
3441
|
self.event = None
|
3443
3442
|
|
3444
3443
|
def set_uid(self) -> ta.Optional[str]:
|
@@ -3604,7 +3603,7 @@ class ProcessGroup:
|
|
3604
3603
|
# ../supervisor.py
|
3605
3604
|
|
3606
3605
|
|
3607
|
-
def timeslice(period, when):
|
3606
|
+
def timeslice(period: int, when: float) -> int:
|
3608
3607
|
return int(when - (when % period))
|
3609
3608
|
|
3610
3609
|
|
@@ -3620,6 +3619,8 @@ class Supervisor:
|
|
3620
3619
|
self._stopping = False # set after we detect that we are handling a stop request
|
3621
3620
|
self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
|
3622
3621
|
|
3622
|
+
#
|
3623
|
+
|
3623
3624
|
@property
|
3624
3625
|
def context(self) -> ServerContext:
|
3625
3626
|
return self._context
|
@@ -3627,58 +3628,7 @@ class Supervisor:
|
|
3627
3628
|
def get_state(self) -> SupervisorState:
|
3628
3629
|
return self._context.state
|
3629
3630
|
|
3630
|
-
|
3631
|
-
self.setup()
|
3632
|
-
self.run()
|
3633
|
-
|
3634
|
-
@cached_nullary
|
3635
|
-
def setup(self) -> None:
|
3636
|
-
if not self._context.first:
|
3637
|
-
# prevent crash on libdispatch-based systems, at least for the first request
|
3638
|
-
self._context.cleanup_fds()
|
3639
|
-
|
3640
|
-
self._context.set_uid_or_exit()
|
3641
|
-
|
3642
|
-
if self._context.first:
|
3643
|
-
self._context.set_rlimits_or_exit()
|
3644
|
-
|
3645
|
-
# this sets the options.logger object delay logger instantiation until after setuid
|
3646
|
-
if not self._context.config.nocleanup:
|
3647
|
-
# clean up old automatic logs
|
3648
|
-
self._context.clear_auto_child_logdir()
|
3649
|
-
|
3650
|
-
def run(
|
3651
|
-
self,
|
3652
|
-
*,
|
3653
|
-
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
3654
|
-
) -> None:
|
3655
|
-
self._process_groups = {} # clear
|
3656
|
-
self._stop_groups = None # clear
|
3657
|
-
|
3658
|
-
clear_events()
|
3659
|
-
|
3660
|
-
try:
|
3661
|
-
for config in self._context.config.groups or []:
|
3662
|
-
self.add_process_group(config)
|
3663
|
-
|
3664
|
-
self._context.set_signals()
|
3665
|
-
|
3666
|
-
if not self._context.config.nodaemon and self._context.first:
|
3667
|
-
self._context.daemonize()
|
3668
|
-
|
3669
|
-
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
3670
|
-
self._context.write_pidfile()
|
3671
|
-
|
3672
|
-
notify_event(SupervisorRunningEvent())
|
3673
|
-
|
3674
|
-
while True:
|
3675
|
-
if callback is not None and not callback(self):
|
3676
|
-
break
|
3677
|
-
|
3678
|
-
self._run_once()
|
3679
|
-
|
3680
|
-
finally:
|
3681
|
-
self._context.cleanup()
|
3631
|
+
#
|
3682
3632
|
|
3683
3633
|
class DiffToActive(ta.NamedTuple):
|
3684
3634
|
added: ta.List[ProcessGroupConfig]
|
@@ -3707,7 +3657,7 @@ class Supervisor:
|
|
3707
3657
|
group = self._process_groups[name] = ProcessGroup(config, self._context)
|
3708
3658
|
group.after_setuid()
|
3709
3659
|
|
3710
|
-
|
3660
|
+
EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
|
3711
3661
|
return True
|
3712
3662
|
|
3713
3663
|
def remove_process_group(self, name: str) -> bool:
|
@@ -3718,7 +3668,7 @@ class Supervisor:
|
|
3718
3668
|
|
3719
3669
|
del self._process_groups[name]
|
3720
3670
|
|
3721
|
-
|
3671
|
+
EVENT_CALLBACKS.notify(ProcessGroupRemovedEvent(name))
|
3722
3672
|
return True
|
3723
3673
|
|
3724
3674
|
def get_process_map(self) -> ta.Dict[int, Dispatcher]:
|
@@ -3747,6 +3697,72 @@ class Supervisor:
|
|
3747
3697
|
|
3748
3698
|
return unstopped
|
3749
3699
|
|
3700
|
+
#
|
3701
|
+
|
3702
|
+
def main(self) -> None:
|
3703
|
+
self.setup()
|
3704
|
+
self.run()
|
3705
|
+
|
3706
|
+
@cached_nullary
|
3707
|
+
def setup(self) -> None:
|
3708
|
+
if not self._context.first:
|
3709
|
+
# prevent crash on libdispatch-based systems, at least for the first request
|
3710
|
+
self._context.cleanup_fds()
|
3711
|
+
|
3712
|
+
self._context.set_uid_or_exit()
|
3713
|
+
|
3714
|
+
if self._context.first:
|
3715
|
+
self._context.set_rlimits_or_exit()
|
3716
|
+
|
3717
|
+
# this sets the options.logger object delay logger instantiation until after setuid
|
3718
|
+
if not self._context.config.nocleanup:
|
3719
|
+
# clean up old automatic logs
|
3720
|
+
self._context.clear_auto_child_logdir()
|
3721
|
+
|
3722
|
+
def run(
|
3723
|
+
self,
|
3724
|
+
*,
|
3725
|
+
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
3726
|
+
) -> None:
|
3727
|
+
self._process_groups = {} # clear
|
3728
|
+
self._stop_groups = None # clear
|
3729
|
+
|
3730
|
+
EVENT_CALLBACKS.clear()
|
3731
|
+
|
3732
|
+
try:
|
3733
|
+
for config in self._context.config.groups or []:
|
3734
|
+
self.add_process_group(config)
|
3735
|
+
|
3736
|
+
self._context.set_signals()
|
3737
|
+
|
3738
|
+
if not self._context.config.nodaemon and self._context.first:
|
3739
|
+
self._context.daemonize()
|
3740
|
+
|
3741
|
+
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
3742
|
+
self._context.write_pidfile()
|
3743
|
+
|
3744
|
+
EVENT_CALLBACKS.notify(SupervisorRunningEvent())
|
3745
|
+
|
3746
|
+
while True:
|
3747
|
+
if callback is not None and not callback(self):
|
3748
|
+
break
|
3749
|
+
|
3750
|
+
self._run_once()
|
3751
|
+
|
3752
|
+
finally:
|
3753
|
+
self._context.cleanup()
|
3754
|
+
|
3755
|
+
#
|
3756
|
+
|
3757
|
+
def _run_once(self) -> None:
|
3758
|
+
self._poll()
|
3759
|
+
self._reap()
|
3760
|
+
self._handle_signal()
|
3761
|
+
self._tick()
|
3762
|
+
|
3763
|
+
if self._context.state < SupervisorStates.RUNNING:
|
3764
|
+
self._ordered_stop_groups_phase_2()
|
3765
|
+
|
3750
3766
|
def _ordered_stop_groups_phase_1(self) -> None:
|
3751
3767
|
if self._stop_groups:
|
3752
3768
|
# stop the last group (the one with the "highest" priority)
|
@@ -3763,7 +3779,7 @@ class Supervisor:
|
|
3763
3779
|
# down, so push it back on to the end of the stop group queue
|
3764
3780
|
self._stop_groups.append(group)
|
3765
3781
|
|
3766
|
-
def
|
3782
|
+
def _poll(self) -> None:
|
3767
3783
|
combined_map = {}
|
3768
3784
|
combined_map.update(self.get_process_map())
|
3769
3785
|
|
@@ -3775,7 +3791,7 @@ class Supervisor:
|
|
3775
3791
|
# first time, set the stopping flag, do a notification and set stop_groups
|
3776
3792
|
self._stopping = True
|
3777
3793
|
self._stop_groups = pgroups[:]
|
3778
|
-
|
3794
|
+
EVENT_CALLBACKS.notify(SupervisorStoppingEvent())
|
3779
3795
|
|
3780
3796
|
self._ordered_stop_groups_phase_1()
|
3781
3797
|
|
@@ -3835,33 +3851,6 @@ class Supervisor:
|
|
3835
3851
|
for group in pgroups:
|
3836
3852
|
group.transition()
|
3837
3853
|
|
3838
|
-
self._reap()
|
3839
|
-
self._handle_signal()
|
3840
|
-
self._tick()
|
3841
|
-
|
3842
|
-
if self._context.state < SupervisorStates.RUNNING:
|
3843
|
-
self._ordered_stop_groups_phase_2()
|
3844
|
-
|
3845
|
-
def _tick(self, now: ta.Optional[float] = None) -> None:
|
3846
|
-
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
3847
|
-
|
3848
|
-
if now is None:
|
3849
|
-
# now won't be None in unit tests
|
3850
|
-
now = time.time()
|
3851
|
-
|
3852
|
-
for event in TICK_EVENTS:
|
3853
|
-
period = event.period # type: ignore
|
3854
|
-
|
3855
|
-
last_tick = self._ticks.get(period)
|
3856
|
-
if last_tick is None:
|
3857
|
-
# we just started up
|
3858
|
-
last_tick = self._ticks[period] = timeslice(period, now)
|
3859
|
-
|
3860
|
-
this_tick = timeslice(period, now)
|
3861
|
-
if this_tick != last_tick:
|
3862
|
-
self._ticks[period] = this_tick
|
3863
|
-
notify_event(event(this_tick, self))
|
3864
|
-
|
3865
3854
|
def _reap(self, *, once: bool = False, depth: int = 0) -> None:
|
3866
3855
|
if depth >= 100:
|
3867
3856
|
return
|
@@ -3910,6 +3899,26 @@ class Supervisor:
|
|
3910
3899
|
else:
|
3911
3900
|
log.debug('received %s indicating nothing', signame(sig))
|
3912
3901
|
|
3902
|
+
def _tick(self, now: ta.Optional[float] = None) -> None:
|
3903
|
+
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
3904
|
+
|
3905
|
+
if now is None:
|
3906
|
+
# now won't be None in unit tests
|
3907
|
+
now = time.time()
|
3908
|
+
|
3909
|
+
for event in TICK_EVENTS:
|
3910
|
+
period = event.period # type: ignore
|
3911
|
+
|
3912
|
+
last_tick = self._ticks.get(period)
|
3913
|
+
if last_tick is None:
|
3914
|
+
# we just started up
|
3915
|
+
last_tick = self._ticks[period] = timeslice(period, now)
|
3916
|
+
|
3917
|
+
this_tick = timeslice(period, now)
|
3918
|
+
if this_tick != last_tick:
|
3919
|
+
self._ticks[period] = this_tick
|
3920
|
+
EVENT_CALLBACKS.notify(event(this_tick, self))
|
3921
|
+
|
3913
3922
|
|
3914
3923
|
########################################
|
3915
3924
|
# main.py
|
@@ -13,9 +13,9 @@ from .compat import find_prefix_at_end
|
|
13
13
|
from .compat import readfd
|
14
14
|
from .compat import strip_escapes
|
15
15
|
from .configs import ProcessConfig
|
16
|
+
from .events import EVENT_CALLBACKS
|
16
17
|
from .events import ProcessLogStderrEvent
|
17
18
|
from .events import ProcessLogStdoutEvent
|
18
|
-
from .events import notify_event
|
19
19
|
from .types import AbstractSubprocess
|
20
20
|
|
21
21
|
|
@@ -207,10 +207,10 @@ class OutputDispatcher(Dispatcher):
|
|
207
207
|
|
208
208
|
if self._channel == 'stdout':
|
209
209
|
if self._stdout_events_enabled:
|
210
|
-
|
210
|
+
EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
211
211
|
|
212
212
|
elif self._stderr_events_enabled:
|
213
|
-
|
213
|
+
EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
214
214
|
|
215
215
|
def record_output(self):
|
216
216
|
if self._capture_log is None:
|
@@ -261,7 +261,7 @@ class OutputDispatcher(Dispatcher):
|
|
261
261
|
channel = self._channel
|
262
262
|
procname = self._process.config.name
|
263
263
|
event = self.event_type(self._process, self._process.pid, data)
|
264
|
-
|
264
|
+
EVENT_CALLBACKS.notify(event)
|
265
265
|
|
266
266
|
log.debug('%r %s emitted a comm event', procname, channel)
|
267
267
|
for handler in self._capture_log.handlers:
|
ominfra/supervisor/events.py
CHANGED
@@ -29,12 +29,9 @@ class EventCallbacks:
|
|
29
29
|
|
30
30
|
EVENT_CALLBACKS = EventCallbacks()
|
31
31
|
|
32
|
-
notify_event = EVENT_CALLBACKS.notify
|
33
|
-
clear_events = EVENT_CALLBACKS.clear
|
34
|
-
|
35
32
|
|
36
33
|
class Event(abc.ABC): # noqa
|
37
|
-
"""Abstract event type
|
34
|
+
"""Abstract event type."""
|
38
35
|
|
39
36
|
|
40
37
|
class ProcessLogEvent(Event, abc.ABC):
|
@@ -114,7 +111,7 @@ class RemoteCommunicationEvent(Event):
|
|
114
111
|
|
115
112
|
|
116
113
|
class SupervisorStateChangeEvent(Event):
|
117
|
-
"""
|
114
|
+
"""Abstract class."""
|
118
115
|
|
119
116
|
def payload(self):
|
120
117
|
return ''
|
@@ -136,7 +133,7 @@ class EventRejectedEvent: # purposely does not subclass Event
|
|
136
133
|
|
137
134
|
|
138
135
|
class ProcessStateEvent(Event):
|
139
|
-
"""
|
136
|
+
"""Abstract class, never raised directly."""
|
140
137
|
frm = None
|
141
138
|
to = None
|
142
139
|
|
@@ -225,7 +222,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent):
|
|
225
222
|
|
226
223
|
|
227
224
|
class TickEvent(Event):
|
228
|
-
"""
|
225
|
+
"""Abstract."""
|
229
226
|
|
230
227
|
def __init__(self, when, supervisord):
|
231
228
|
super().__init__()
|
ominfra/supervisor/process.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import errno
|
3
3
|
import functools
|
4
|
-
import os
|
4
|
+
import os.path
|
5
5
|
import shlex
|
6
6
|
import signal
|
7
7
|
import time
|
@@ -30,6 +30,7 @@ from .datatypes import RestartUnconditionally
|
|
30
30
|
from .dispatchers import Dispatcher
|
31
31
|
from .dispatchers import InputDispatcher
|
32
32
|
from .dispatchers import OutputDispatcher
|
33
|
+
from .events import EVENT_CALLBACKS
|
33
34
|
from .events import EventRejectedEvent
|
34
35
|
from .events import ProcessCommunicationEvent
|
35
36
|
from .events import ProcessCommunicationStderrEvent
|
@@ -43,7 +44,6 @@ from .events import ProcessStateStartingEvent
|
|
43
44
|
from .events import ProcessStateStoppedEvent
|
44
45
|
from .events import ProcessStateStoppingEvent
|
45
46
|
from .events import ProcessStateUnknownEvent
|
46
|
-
from .events import notify_event
|
47
47
|
from .exceptions import BadCommandError
|
48
48
|
from .exceptions import ProcessError
|
49
49
|
from .states import STOPPED_STATES
|
@@ -207,7 +207,7 @@ class Subprocess(AbstractSubprocess):
|
|
207
207
|
event_class = self.event_map.get(new_state)
|
208
208
|
if event_class is not None:
|
209
209
|
event = event_class(self, old_state, expected)
|
210
|
-
|
210
|
+
EVENT_CALLBACKS.notify(event)
|
211
211
|
|
212
212
|
return True
|
213
213
|
|
@@ -324,6 +324,7 @@ class Subprocess(AbstractSubprocess):
|
|
324
324
|
os.dup2(self._pipes['child_stdout'], 2)
|
325
325
|
else:
|
326
326
|
os.dup2(self._pipes['child_stderr'], 2)
|
327
|
+
# FIXME: leave debugger fds
|
327
328
|
for i in range(3, self.context.config.minfds):
|
328
329
|
close_fd(i)
|
329
330
|
|
@@ -359,7 +360,7 @@ class Subprocess(AbstractSubprocess):
|
|
359
360
|
cwd = self.config.directory
|
360
361
|
try:
|
361
362
|
if cwd is not None:
|
362
|
-
os.chdir(cwd)
|
363
|
+
os.chdir(os.path.expanduser(cwd))
|
363
364
|
except OSError as why:
|
364
365
|
code = errno.errorcode.get(why.args[0], why.args[0])
|
365
366
|
msg = f"couldn't chdir to {cwd}: {code}\n"
|
@@ -415,7 +416,7 @@ class Subprocess(AbstractSubprocess):
|
|
415
416
|
return self.kill(self.config.stopsignal)
|
416
417
|
|
417
418
|
def stop_report(self) -> None:
|
418
|
-
"""
|
419
|
+
"""Log a 'waiting for x to stop' message with throttling."""
|
419
420
|
if self.state == ProcessStates.STOPPING:
|
420
421
|
now = time.time()
|
421
422
|
|
@@ -545,7 +546,7 @@ class Subprocess(AbstractSubprocess):
|
|
545
546
|
return None
|
546
547
|
|
547
548
|
def finish(self, sts: int) -> None:
|
548
|
-
"""
|
549
|
+
"""The process was reaped and we need to report and manage its state."""
|
549
550
|
|
550
551
|
self.drain()
|
551
552
|
|
@@ -626,7 +627,7 @@ class Subprocess(AbstractSubprocess):
|
|
626
627
|
# system that this event was rejected so it can be processed again.
|
627
628
|
if self.event is not None:
|
628
629
|
# Note: this should only be true if we were in the BUSY state when finish() was called.
|
629
|
-
|
630
|
+
EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
|
630
631
|
self.event = None
|
631
632
|
|
632
633
|
def set_uid(self) -> ta.Optional[str]:
|
ominfra/supervisor/supervisor.py
CHANGED
@@ -14,13 +14,12 @@ from .compat import signame
|
|
14
14
|
from .configs import ProcessGroupConfig
|
15
15
|
from .context import ServerContext
|
16
16
|
from .dispatchers import Dispatcher
|
17
|
+
from .events import EVENT_CALLBACKS
|
17
18
|
from .events import TICK_EVENTS
|
18
19
|
from .events import ProcessGroupAddedEvent
|
19
20
|
from .events import ProcessGroupRemovedEvent
|
20
21
|
from .events import SupervisorRunningEvent
|
21
22
|
from .events import SupervisorStoppingEvent
|
22
|
-
from .events import clear_events
|
23
|
-
from .events import notify_event
|
24
23
|
from .process import ProcessGroup
|
25
24
|
from .process import Subprocess
|
26
25
|
from .states import SupervisorState
|
@@ -28,7 +27,7 @@ from .states import SupervisorStates
|
|
28
27
|
from .states import get_process_state_description
|
29
28
|
|
30
29
|
|
31
|
-
def timeslice(period, when):
|
30
|
+
def timeslice(period: int, when: float) -> int:
|
32
31
|
return int(when - (when % period))
|
33
32
|
|
34
33
|
|
@@ -44,6 +43,8 @@ class Supervisor:
|
|
44
43
|
self._stopping = False # set after we detect that we are handling a stop request
|
45
44
|
self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
|
46
45
|
|
46
|
+
#
|
47
|
+
|
47
48
|
@property
|
48
49
|
def context(self) -> ServerContext:
|
49
50
|
return self._context
|
@@ -51,58 +52,7 @@ class Supervisor:
|
|
51
52
|
def get_state(self) -> SupervisorState:
|
52
53
|
return self._context.state
|
53
54
|
|
54
|
-
|
55
|
-
self.setup()
|
56
|
-
self.run()
|
57
|
-
|
58
|
-
@cached_nullary
|
59
|
-
def setup(self) -> None:
|
60
|
-
if not self._context.first:
|
61
|
-
# prevent crash on libdispatch-based systems, at least for the first request
|
62
|
-
self._context.cleanup_fds()
|
63
|
-
|
64
|
-
self._context.set_uid_or_exit()
|
65
|
-
|
66
|
-
if self._context.first:
|
67
|
-
self._context.set_rlimits_or_exit()
|
68
|
-
|
69
|
-
# this sets the options.logger object delay logger instantiation until after setuid
|
70
|
-
if not self._context.config.nocleanup:
|
71
|
-
# clean up old automatic logs
|
72
|
-
self._context.clear_auto_child_logdir()
|
73
|
-
|
74
|
-
def run(
|
75
|
-
self,
|
76
|
-
*,
|
77
|
-
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
78
|
-
) -> None:
|
79
|
-
self._process_groups = {} # clear
|
80
|
-
self._stop_groups = None # clear
|
81
|
-
|
82
|
-
clear_events()
|
83
|
-
|
84
|
-
try:
|
85
|
-
for config in self._context.config.groups or []:
|
86
|
-
self.add_process_group(config)
|
87
|
-
|
88
|
-
self._context.set_signals()
|
89
|
-
|
90
|
-
if not self._context.config.nodaemon and self._context.first:
|
91
|
-
self._context.daemonize()
|
92
|
-
|
93
|
-
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
94
|
-
self._context.write_pidfile()
|
95
|
-
|
96
|
-
notify_event(SupervisorRunningEvent())
|
97
|
-
|
98
|
-
while True:
|
99
|
-
if callback is not None and not callback(self):
|
100
|
-
break
|
101
|
-
|
102
|
-
self._run_once()
|
103
|
-
|
104
|
-
finally:
|
105
|
-
self._context.cleanup()
|
55
|
+
#
|
106
56
|
|
107
57
|
class DiffToActive(ta.NamedTuple):
|
108
58
|
added: ta.List[ProcessGroupConfig]
|
@@ -131,7 +81,7 @@ class Supervisor:
|
|
131
81
|
group = self._process_groups[name] = ProcessGroup(config, self._context)
|
132
82
|
group.after_setuid()
|
133
83
|
|
134
|
-
|
84
|
+
EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
|
135
85
|
return True
|
136
86
|
|
137
87
|
def remove_process_group(self, name: str) -> bool:
|
@@ -142,7 +92,7 @@ class Supervisor:
|
|
142
92
|
|
143
93
|
del self._process_groups[name]
|
144
94
|
|
145
|
-
|
95
|
+
EVENT_CALLBACKS.notify(ProcessGroupRemovedEvent(name))
|
146
96
|
return True
|
147
97
|
|
148
98
|
def get_process_map(self) -> ta.Dict[int, Dispatcher]:
|
@@ -171,6 +121,72 @@ class Supervisor:
|
|
171
121
|
|
172
122
|
return unstopped
|
173
123
|
|
124
|
+
#
|
125
|
+
|
126
|
+
def main(self) -> None:
|
127
|
+
self.setup()
|
128
|
+
self.run()
|
129
|
+
|
130
|
+
@cached_nullary
|
131
|
+
def setup(self) -> None:
|
132
|
+
if not self._context.first:
|
133
|
+
# prevent crash on libdispatch-based systems, at least for the first request
|
134
|
+
self._context.cleanup_fds()
|
135
|
+
|
136
|
+
self._context.set_uid_or_exit()
|
137
|
+
|
138
|
+
if self._context.first:
|
139
|
+
self._context.set_rlimits_or_exit()
|
140
|
+
|
141
|
+
# this sets the options.logger object delay logger instantiation until after setuid
|
142
|
+
if not self._context.config.nocleanup:
|
143
|
+
# clean up old automatic logs
|
144
|
+
self._context.clear_auto_child_logdir()
|
145
|
+
|
146
|
+
def run(
|
147
|
+
self,
|
148
|
+
*,
|
149
|
+
callback: ta.Optional[ta.Callable[['Supervisor'], bool]] = None,
|
150
|
+
) -> None:
|
151
|
+
self._process_groups = {} # clear
|
152
|
+
self._stop_groups = None # clear
|
153
|
+
|
154
|
+
EVENT_CALLBACKS.clear()
|
155
|
+
|
156
|
+
try:
|
157
|
+
for config in self._context.config.groups or []:
|
158
|
+
self.add_process_group(config)
|
159
|
+
|
160
|
+
self._context.set_signals()
|
161
|
+
|
162
|
+
if not self._context.config.nodaemon and self._context.first:
|
163
|
+
self._context.daemonize()
|
164
|
+
|
165
|
+
# writing pid file needs to come *after* daemonizing or pid will be wrong
|
166
|
+
self._context.write_pidfile()
|
167
|
+
|
168
|
+
EVENT_CALLBACKS.notify(SupervisorRunningEvent())
|
169
|
+
|
170
|
+
while True:
|
171
|
+
if callback is not None and not callback(self):
|
172
|
+
break
|
173
|
+
|
174
|
+
self._run_once()
|
175
|
+
|
176
|
+
finally:
|
177
|
+
self._context.cleanup()
|
178
|
+
|
179
|
+
#
|
180
|
+
|
181
|
+
def _run_once(self) -> None:
|
182
|
+
self._poll()
|
183
|
+
self._reap()
|
184
|
+
self._handle_signal()
|
185
|
+
self._tick()
|
186
|
+
|
187
|
+
if self._context.state < SupervisorStates.RUNNING:
|
188
|
+
self._ordered_stop_groups_phase_2()
|
189
|
+
|
174
190
|
def _ordered_stop_groups_phase_1(self) -> None:
|
175
191
|
if self._stop_groups:
|
176
192
|
# stop the last group (the one with the "highest" priority)
|
@@ -187,7 +203,7 @@ class Supervisor:
|
|
187
203
|
# down, so push it back on to the end of the stop group queue
|
188
204
|
self._stop_groups.append(group)
|
189
205
|
|
190
|
-
def
|
206
|
+
def _poll(self) -> None:
|
191
207
|
combined_map = {}
|
192
208
|
combined_map.update(self.get_process_map())
|
193
209
|
|
@@ -199,7 +215,7 @@ class Supervisor:
|
|
199
215
|
# first time, set the stopping flag, do a notification and set stop_groups
|
200
216
|
self._stopping = True
|
201
217
|
self._stop_groups = pgroups[:]
|
202
|
-
|
218
|
+
EVENT_CALLBACKS.notify(SupervisorStoppingEvent())
|
203
219
|
|
204
220
|
self._ordered_stop_groups_phase_1()
|
205
221
|
|
@@ -259,33 +275,6 @@ class Supervisor:
|
|
259
275
|
for group in pgroups:
|
260
276
|
group.transition()
|
261
277
|
|
262
|
-
self._reap()
|
263
|
-
self._handle_signal()
|
264
|
-
self._tick()
|
265
|
-
|
266
|
-
if self._context.state < SupervisorStates.RUNNING:
|
267
|
-
self._ordered_stop_groups_phase_2()
|
268
|
-
|
269
|
-
def _tick(self, now: ta.Optional[float] = None) -> None:
|
270
|
-
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
271
|
-
|
272
|
-
if now is None:
|
273
|
-
# now won't be None in unit tests
|
274
|
-
now = time.time()
|
275
|
-
|
276
|
-
for event in TICK_EVENTS:
|
277
|
-
period = event.period # type: ignore
|
278
|
-
|
279
|
-
last_tick = self._ticks.get(period)
|
280
|
-
if last_tick is None:
|
281
|
-
# we just started up
|
282
|
-
last_tick = self._ticks[period] = timeslice(period, now)
|
283
|
-
|
284
|
-
this_tick = timeslice(period, now)
|
285
|
-
if this_tick != last_tick:
|
286
|
-
self._ticks[period] = this_tick
|
287
|
-
notify_event(event(this_tick, self))
|
288
|
-
|
289
278
|
def _reap(self, *, once: bool = False, depth: int = 0) -> None:
|
290
279
|
if depth >= 100:
|
291
280
|
return
|
@@ -333,3 +322,23 @@ class Supervisor:
|
|
333
322
|
|
334
323
|
else:
|
335
324
|
log.debug('received %s indicating nothing', signame(sig))
|
325
|
+
|
326
|
+
def _tick(self, now: ta.Optional[float] = None) -> None:
|
327
|
+
"""Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
|
328
|
+
|
329
|
+
if now is None:
|
330
|
+
# now won't be None in unit tests
|
331
|
+
now = time.time()
|
332
|
+
|
333
|
+
for event in TICK_EVENTS:
|
334
|
+
period = event.period # type: ignore
|
335
|
+
|
336
|
+
last_tick = self._ticks.get(period)
|
337
|
+
if last_tick is None:
|
338
|
+
# we just started up
|
339
|
+
last_tick = self._ticks[period] = timeslice(period, now)
|
340
|
+
|
341
|
+
this_tick = timeslice(period, now)
|
342
|
+
if this_tick != last_tick:
|
343
|
+
self._ticks[period] = this_tick
|
344
|
+
EVENT_CALLBACKS.notify(event(this_tick, self))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ominfra
|
3
|
-
Version: 0.0.0.
|
3
|
+
Version: 0.0.0.dev120
|
4
4
|
Summary: ominfra
|
5
5
|
Author: wrmsr
|
6
6
|
License: BSD-3-Clause
|
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Operating System :: POSIX
|
13
13
|
Requires-Python: >=3.12
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: omdev ==0.0.0.
|
16
|
-
Requires-Dist: omlish ==0.0.0.
|
15
|
+
Requires-Dist: omdev ==0.0.0.dev120
|
16
|
+
Requires-Dist: omlish ==0.0.0.dev120
|
17
17
|
Provides-Extra: all
|
18
18
|
Requires-Dist: paramiko ~=3.5 ; extra == 'all'
|
19
19
|
Requires-Dist: asyncssh ~=2.18 ; extra == 'all'
|
@@ -59,30 +59,30 @@ ominfra/pyremote/bootstrap.py,sha256=RvMO3YGaN1E4sgUi1JEtiPak8cjvqtc_vRCq1yqbeZg
|
|
59
59
|
ominfra/pyremote/runcommands.py,sha256=bviS0_TDIoZVAe4h-_iavbvJtVSFu8lnk7fQ5iasCWE,1571
|
60
60
|
ominfra/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
61
61
|
ominfra/scripts/journald2aws.py,sha256=0HHYi_uBV1t2KefVrExs3IZ6Zy-mQa7xN_ka9W9Obb8,94910
|
62
|
-
ominfra/scripts/supervisor.py,sha256=
|
62
|
+
ominfra/scripts/supervisor.py,sha256=C1eT7pIqPRJgN4U87tOjR_SuOSUwd5aUswvPeTy5Xlw,121831
|
63
63
|
ominfra/supervisor/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
|
64
64
|
ominfra/supervisor/__main__.py,sha256=I0yFw-C08OOiZ3BF6lF1Oiv789EQXu-_j6whDhQUTEA,66
|
65
65
|
ominfra/supervisor/compat.py,sha256=Y1d_pk4eN18AbVYjDHAXMMnPwOKTFpc7JDb1uClYMsQ,5064
|
66
66
|
ominfra/supervisor/configs.py,sha256=KpibZJ-V-4UpoJM2fnjXOXJLvDbwRJzNLXLGESUljV4,2966
|
67
67
|
ominfra/supervisor/context.py,sha256=Fg_wD6oUR8vxe3JMM14mR-5ssIrTNwxRr-AXfoGbzJQ,15795
|
68
68
|
ominfra/supervisor/datatypes.py,sha256=UnXO_UlCyJD9u0uvea1wvnk_UZCzxNMeFvPK83gv530,4432
|
69
|
-
ominfra/supervisor/dispatchers.py,sha256=
|
70
|
-
ominfra/supervisor/events.py,sha256=
|
69
|
+
ominfra/supervisor/dispatchers.py,sha256=sJ61yTo9EEbxHwe2NbzOTAFFFCuuyIhYli_xJioQBoo,10423
|
70
|
+
ominfra/supervisor/events.py,sha256=IhdL7Fj-hEvTvZ5WF6aIa2YjSPQhuUoasoJSMmRLQkU,7650
|
71
71
|
ominfra/supervisor/exceptions.py,sha256=Qbu211H3CLlSmi9LsSikOwrcL5HgJP9ugvcKWlGTAoI,750
|
72
72
|
ominfra/supervisor/main.py,sha256=0bj_9AzIfDlB1BB8zcX9npQIknamN7FGoVEYgLMLuP0,1701
|
73
73
|
ominfra/supervisor/poller.py,sha256=VCBxLItfA4Vj69jet2XFbFScPbmdD9JA1evaofk_AnY,7709
|
74
|
-
ominfra/supervisor/process.py,sha256=
|
74
|
+
ominfra/supervisor/process.py,sha256=94cglin7qBwxTNXjOBxqec4qsACu-VfeboW-JfzvvbE,31454
|
75
75
|
ominfra/supervisor/states.py,sha256=JMxXYTZhJkMNQZ2tTV6wId7wrvnWgiZteskACprKskM,1374
|
76
|
-
ominfra/supervisor/supervisor.py,sha256=
|
76
|
+
ominfra/supervisor/supervisor.py,sha256=p612yph5aKzkVLH6CfOMcRHEbNfs_TMlmjfLwtG8Jo0,12324
|
77
77
|
ominfra/supervisor/types.py,sha256=ec62QG0CDJc0XNxCnf3lXxhsxrr4CCScLPI-1SpQjlc,1141
|
78
78
|
ominfra/tailscale/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
79
79
|
ominfra/tailscale/api.py,sha256=C5-t_b6jZXUWcy5k8bXm7CFnk73pSdrlMOgGDeGVrpw,1370
|
80
80
|
ominfra/tailscale/cli.py,sha256=DSGp4hn5xwOW-l_u_InKlSF6kIobxtUtVssf_73STs0,3567
|
81
81
|
ominfra/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
82
82
|
ominfra/tools/listresources.py,sha256=4qVg5txsb10EHhvqXXeM6gJ2jx9LbroEnPydDv1uXs0,6176
|
83
|
-
ominfra-0.0.0.
|
84
|
-
ominfra-0.0.0.
|
85
|
-
ominfra-0.0.0.
|
86
|
-
ominfra-0.0.0.
|
87
|
-
ominfra-0.0.0.
|
88
|
-
ominfra-0.0.0.
|
83
|
+
ominfra-0.0.0.dev120.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
84
|
+
ominfra-0.0.0.dev120.dist-info/METADATA,sha256=xuFwZN7LnvBCDHvhqgdKONRKkDYUYJ6SZqTjaE4k2rI,742
|
85
|
+
ominfra-0.0.0.dev120.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
86
|
+
ominfra-0.0.0.dev120.dist-info/entry_points.txt,sha256=kgecQ2MgGrM9qK744BoKS3tMesaC3yjLnl9pa5CRczg,37
|
87
|
+
ominfra-0.0.0.dev120.dist-info/top_level.txt,sha256=E-b2OHkk_AOBLXHYZQ2EOFKl-_6uOGd8EjeG-Zy6h_w,8
|
88
|
+
ominfra-0.0.0.dev120.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|