ominfra 0.0.0.dev118__py3-none-any.whl → 0.0.0.dev120__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.
@@ -2,12 +2,13 @@
2
2
  # noinspection DuplicatedCode
3
3
  # @omlish-lite
4
4
  # @omlish-script
5
- # @omlish-amalg-output ../supervisor/supervisor.py
6
- # ruff: noqa: N802 UP006 UP007 UP036
5
+ # @omlish-amalg-output ../supervisor/main.py
6
+ # ruff: noqa: N802 UP006 UP007 UP012 UP036
7
7
  import abc
8
8
  import base64
9
9
  import collections.abc
10
10
  import contextlib
11
+ import ctypes as ct
11
12
  import dataclasses as dc
12
13
  import datetime
13
14
  import decimal
@@ -17,9 +18,11 @@ import fcntl
17
18
  import fractions
18
19
  import functools
19
20
  import grp
21
+ import itertools
20
22
  import json
21
23
  import logging
22
24
  import os
25
+ import os.path
23
26
  import pwd
24
27
  import re
25
28
  import resource
@@ -28,6 +31,7 @@ import shlex
28
31
  import signal
29
32
  import stat
30
33
  import sys
34
+ import syslog
31
35
  import tempfile
32
36
  import threading
33
37
  import time
@@ -466,240 +470,28 @@ class NoPermissionError(ProcessError):
466
470
 
467
471
 
468
472
  ########################################
469
- # ../poller.py
470
-
471
-
472
- log = logging.getLogger(__name__)
473
-
474
-
475
- class BasePoller(abc.ABC):
476
-
477
- def __init__(self) -> None:
478
- super().__init__()
479
-
480
- @abc.abstractmethod
481
- def register_readable(self, fd: int) -> None:
482
- raise NotImplementedError
483
-
484
- @abc.abstractmethod
485
- def register_writable(self, fd: int) -> None:
486
- raise NotImplementedError
487
-
488
- @abc.abstractmethod
489
- def unregister_readable(self, fd: int) -> None:
490
- raise NotImplementedError
491
-
492
- @abc.abstractmethod
493
- def unregister_writable(self, fd: int) -> None:
494
- raise NotImplementedError
495
-
496
- @abc.abstractmethod
497
- def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
498
- raise NotImplementedError
499
-
500
- def before_daemonize(self) -> None: # noqa
501
- pass
502
-
503
- def after_daemonize(self) -> None: # noqa
504
- pass
505
-
506
- def close(self) -> None: # noqa
507
- pass
508
-
509
-
510
- class SelectPoller(BasePoller):
511
-
512
- def __init__(self) -> None:
513
- super().__init__()
514
-
515
- self._readables: ta.Set[int] = set()
516
- self._writables: ta.Set[int] = set()
517
-
518
- def register_readable(self, fd: int) -> None:
519
- self._readables.add(fd)
520
-
521
- def register_writable(self, fd: int) -> None:
522
- self._writables.add(fd)
523
-
524
- def unregister_readable(self, fd: int) -> None:
525
- self._readables.discard(fd)
526
-
527
- def unregister_writable(self, fd: int) -> None:
528
- self._writables.discard(fd)
529
-
530
- def unregister_all(self) -> None:
531
- self._readables.clear()
532
- self._writables.clear()
473
+ # ../../../omlish/lite/cached.py
533
474
 
534
- def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
535
- try:
536
- r, w, x = select.select(
537
- self._readables,
538
- self._writables,
539
- [], timeout,
540
- )
541
- except OSError as err:
542
- if err.args[0] == errno.EINTR:
543
- log.debug('EINTR encountered in poll')
544
- return [], []
545
- if err.args[0] == errno.EBADF:
546
- log.debug('EBADF encountered in poll')
547
- self.unregister_all()
548
- return [], []
549
- raise
550
- return r, w
551
-
552
-
553
- class PollPoller(BasePoller):
554
- _READ = select.POLLIN | select.POLLPRI | select.POLLHUP
555
- _WRITE = select.POLLOUT
556
475
 
557
- def __init__(self) -> None:
476
+ class _cached_nullary: # noqa
477
+ def __init__(self, fn):
558
478
  super().__init__()
479
+ self._fn = fn
480
+ self._value = self._missing = object()
481
+ functools.update_wrapper(self, fn)
559
482
 
560
- self._poller = select.poll()
561
- self._readables: set[int] = set()
562
- self._writables: set[int] = set()
563
-
564
- def register_readable(self, fd: int) -> None:
565
- self._poller.register(fd, self._READ)
566
- self._readables.add(fd)
567
-
568
- def register_writable(self, fd: int) -> None:
569
- self._poller.register(fd, self._WRITE)
570
- self._writables.add(fd)
483
+ def __call__(self, *args, **kwargs): # noqa
484
+ if self._value is self._missing:
485
+ self._value = self._fn()
486
+ return self._value
571
487
 
572
- def unregister_readable(self, fd: int) -> None:
573
- self._readables.discard(fd)
574
- self._poller.unregister(fd)
575
- if fd in self._writables:
576
- self._poller.register(fd, self._WRITE)
488
+ def __get__(self, instance, owner): # noqa
489
+ bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
490
+ return bound
577
491
 
578
- def unregister_writable(self, fd: int) -> None:
579
- self._writables.discard(fd)
580
- self._poller.unregister(fd)
581
- if fd in self._readables:
582
- self._poller.register(fd, self._READ)
583
492
 
584
- def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
585
- fds = self._poll_fds(timeout) # type: ignore
586
- readables, writables = [], []
587
- for fd, eventmask in fds:
588
- if self._ignore_invalid(fd, eventmask):
589
- continue
590
- if eventmask & self._READ:
591
- readables.append(fd)
592
- if eventmask & self._WRITE:
593
- writables.append(fd)
594
- return readables, writables
595
-
596
- def _poll_fds(self, timeout: float) -> ta.List[ta.Tuple[int, int]]:
597
- try:
598
- return self._poller.poll(timeout * 1000)
599
- except OSError as err:
600
- if err.args[0] == errno.EINTR:
601
- log.debug('EINTR encountered in poll')
602
- return []
603
- raise
604
-
605
- def _ignore_invalid(self, fd: int, eventmask: int) -> bool:
606
- if eventmask & select.POLLNVAL:
607
- # POLLNVAL means `fd` value is invalid, not open. When a process quits it's `fd`s are closed so there is no
608
- # more reason to keep this `fd` registered If the process restarts it's `fd`s are registered again.
609
- self._poller.unregister(fd)
610
- self._readables.discard(fd)
611
- self._writables.discard(fd)
612
- return True
613
- return False
614
-
615
-
616
- if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
617
- class KqueuePoller(BasePoller):
618
- max_events = 1000
619
-
620
- def __init__(self) -> None:
621
- super().__init__()
622
-
623
- self._kqueue: ta.Optional[ta.Any] = select.kqueue()
624
- self._readables: set[int] = set()
625
- self._writables: set[int] = set()
626
-
627
- def register_readable(self, fd: int) -> None:
628
- self._readables.add(fd)
629
- kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD)
630
- self._kqueue_control(fd, kevent)
631
-
632
- def register_writable(self, fd: int) -> None:
633
- self._writables.add(fd)
634
- kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_ADD)
635
- self._kqueue_control(fd, kevent)
636
-
637
- def unregister_readable(self, fd: int) -> None:
638
- kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_DELETE)
639
- self._readables.discard(fd)
640
- self._kqueue_control(fd, kevent)
641
-
642
- def unregister_writable(self, fd: int) -> None:
643
- kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_DELETE)
644
- self._writables.discard(fd)
645
- self._kqueue_control(fd, kevent)
646
-
647
- def _kqueue_control(self, fd: int, kevent: 'select.kevent') -> None:
648
- try:
649
- self._kqueue.control([kevent], 0) # type: ignore
650
- except OSError as error:
651
- if error.errno == errno.EBADF:
652
- log.debug('EBADF encountered in kqueue. Invalid file descriptor %s', fd)
653
- else:
654
- raise
655
-
656
- def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
657
- readables, writables = [], [] # type: ignore
658
-
659
- try:
660
- kevents = self._kqueue.control(None, self.max_events, timeout) # type: ignore
661
- except OSError as error:
662
- if error.errno == errno.EINTR:
663
- log.debug('EINTR encountered in poll')
664
- return readables, writables
665
- raise
666
-
667
- for kevent in kevents:
668
- if kevent.filter == select.KQ_FILTER_READ:
669
- readables.append(kevent.ident)
670
- if kevent.filter == select.KQ_FILTER_WRITE:
671
- writables.append(kevent.ident)
672
-
673
- return readables, writables
674
-
675
- def before_daemonize(self) -> None:
676
- self.close()
677
-
678
- def after_daemonize(self) -> None:
679
- self._kqueue = select.kqueue()
680
- for fd in self._readables:
681
- self.register_readable(fd)
682
- for fd in self._writables:
683
- self.register_writable(fd)
684
-
685
- def close(self) -> None:
686
- self._kqueue.close() # type: ignore
687
- self._kqueue = None
688
-
689
- else:
690
- KqueuePoller = None
691
-
692
-
693
- Poller: ta.Type[BasePoller]
694
- if (
695
- sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
696
- hasattr(select, 'kqueue') and KqueuePoller is not None
697
- ):
698
- Poller = KqueuePoller
699
- elif hasattr(select, 'poll'):
700
- Poller = PollPoller
701
- else:
702
- Poller = SelectPoller
493
+ def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
494
+ return _cached_nullary(fn)
703
495
 
704
496
 
705
497
  ########################################
@@ -1012,6 +804,164 @@ def get_supervisor_state_description(code: SupervisorState) -> str:
1012
804
  return check_not_none(_supervisor_states_by_code.get(code))
1013
805
 
1014
806
 
807
+ ########################################
808
+ # ../../../omlish/lite/journald.py
809
+
810
+
811
+ ##
812
+
813
+
814
+ class sd_iovec(ct.Structure): # noqa
815
+ pass
816
+
817
+
818
+ sd_iovec._fields_ = [
819
+ ('iov_base', ct.c_void_p), # Pointer to data.
820
+ ('iov_len', ct.c_size_t), # Length of data.
821
+ ]
822
+
823
+
824
+ ##
825
+
826
+
827
+ @cached_nullary
828
+ def sd_libsystemd() -> ta.Any:
829
+ lib = ct.CDLL('libsystemd.so.0')
830
+
831
+ lib.sd_journal_sendv = lib['sd_journal_sendv'] # type: ignore
832
+ lib.sd_journal_sendv.restype = ct.c_int
833
+ lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
834
+
835
+ return lib
836
+
837
+
838
+ @cached_nullary
839
+ def sd_try_libsystemd() -> ta.Optional[ta.Any]:
840
+ try:
841
+ return sd_libsystemd()
842
+ except OSError: # noqa
843
+ return None
844
+
845
+
846
+ ##
847
+
848
+
849
+ def sd_journald_send(**fields: str) -> int:
850
+ lib = sd_libsystemd()
851
+
852
+ msgs = [
853
+ f'{k.upper()}={v}\0'.encode('utf-8')
854
+ for k, v in fields.items()
855
+ ]
856
+
857
+ vec = (sd_iovec * len(msgs))()
858
+ cl = (ct.c_char_p * len(msgs))() # noqa
859
+ for i in range(len(msgs)):
860
+ vec[i].iov_base = ct.cast(ct.c_char_p(msgs[i]), ct.c_void_p)
861
+ vec[i].iov_len = len(msgs[i]) - 1
862
+
863
+ return lib.sd_journal_sendv(vec, len(msgs))
864
+
865
+
866
+ ##
867
+
868
+
869
+ SD_LOG_LEVEL_MAP: ta.Mapping[int, int] = {
870
+ logging.FATAL: syslog.LOG_EMERG, # system is unusable
871
+ # LOG_ALERT ? # action must be taken immediately
872
+ logging.CRITICAL: syslog.LOG_CRIT,
873
+ logging.ERROR: syslog.LOG_ERR,
874
+ logging.WARNING: syslog.LOG_WARNING,
875
+ # LOG_NOTICE ? # normal but significant condition
876
+ logging.INFO: syslog.LOG_INFO,
877
+ logging.DEBUG: syslog.LOG_DEBUG,
878
+ }
879
+
880
+
881
+ class JournaldLogHandler(logging.Handler):
882
+ """
883
+ TODO:
884
+ - fallback handler for when this barfs
885
+ """
886
+
887
+ def __init__(
888
+ self,
889
+ *,
890
+ use_formatter_output: bool = False,
891
+ ) -> None:
892
+ super().__init__()
893
+
894
+ sd_libsystemd()
895
+
896
+ self._use_formatter_output = use_formatter_output
897
+
898
+ #
899
+
900
+ EXTRA_RECORD_ATTRS_BY_JOURNALD_FIELD: ta.ClassVar[ta.Mapping[str, str]] = {
901
+ 'name': 'name',
902
+ 'module': 'module',
903
+ 'exception': 'exc_text',
904
+ 'thread_name': 'threadName',
905
+ 'task_name': 'taskName',
906
+ }
907
+
908
+ def make_fields(self, record: logging.LogRecord) -> ta.Mapping[str, str]:
909
+ formatter_message = self.format(record)
910
+ if self._use_formatter_output:
911
+ message = formatter_message
912
+ else:
913
+ message = record.message
914
+
915
+ fields: dict[str, str] = {
916
+ 'message': message,
917
+ 'priority': str(SD_LOG_LEVEL_MAP[record.levelno]),
918
+ 'tid': str(threading.get_ident()),
919
+ }
920
+
921
+ if (pathname := record.pathname) is not None:
922
+ fields['code_file'] = pathname
923
+ if (lineno := record.lineno) is not None:
924
+ fields['code_lineno'] = str(lineno)
925
+ if (func_name := record.funcName) is not None:
926
+ fields['code_func'] = func_name
927
+
928
+ for f, a in self.EXTRA_RECORD_ATTRS_BY_JOURNALD_FIELD.items():
929
+ if (v := getattr(record, a, None)) is not None:
930
+ fields[f] = str(v)
931
+
932
+ return fields
933
+
934
+ #
935
+
936
+ def emit(self, record: logging.LogRecord) -> None:
937
+ try:
938
+ fields = self.make_fields(record)
939
+
940
+ if rc := sd_journald_send(**fields):
941
+ raise RuntimeError(f'{self.__class__.__name__}.emit failed: {rc=}') # noqa
942
+
943
+ except RecursionError: # See issue 36272
944
+ raise
945
+
946
+ except Exception: # noqa
947
+ self.handleError(record)
948
+
949
+
950
+ def journald_log_handler_factory(
951
+ *,
952
+ no_tty_check: bool = False,
953
+ no_fallback: bool = False,
954
+ ) -> logging.Handler:
955
+ if (
956
+ sys.platform == 'linux' and
957
+ (no_tty_check or not sys.stderr.isatty()) and
958
+ (no_fallback or sd_try_libsystemd() is not None)
959
+ ):
960
+ return JournaldLogHandler()
961
+
962
+ return logging.StreamHandler()
963
+
964
+
1015
965
  ########################################
1016
966
  # ../../../omlish/lite/logs.py
1017
967
  """
@@ -1099,7 +1049,7 @@ class StandardLogFormatter(logging.Formatter):
1099
1049
  if datefmt:
1100
1050
  return ct.strftime(datefmt) # noqa
1101
1051
  else:
1102
- t = ct.strftime("%Y-%m-%d %H:%M:%S") # noqa
1052
+ t = ct.strftime('%Y-%m-%d %H:%M:%S')
1103
1053
  return '%s.%03d' % (t, record.msecs)
1104
1054
 
1105
1055
 
@@ -1236,6 +1186,7 @@ def configure_standard_logging(
1236
1186
  json: bool = False,
1237
1187
  target: ta.Optional[logging.Logger] = None,
1238
1188
  force: bool = False,
1189
+ handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
1239
1190
  ) -> ta.Optional[StandardLogHandler]:
1240
1191
  with _locking_logging_module_lock():
1241
1192
  if target is None:
@@ -1249,7 +1200,10 @@ def configure_standard_logging(
1249
1200
 
1250
1201
  #
1251
1202
 
1252
- handler = logging.StreamHandler()
1203
+ if handler_factory is not None:
1204
+ handler = handler_factory()
1205
+ else:
1206
+ handler = logging.StreamHandler()
1253
1207
 
1254
1208
  #
1255
1209
 
@@ -1649,12 +1603,9 @@ class EventCallbacks:
1649
1603
 
1650
1604
  EVENT_CALLBACKS = EventCallbacks()
1651
1605
 
1652
- notify_event = EVENT_CALLBACKS.notify
1653
- clear_events = EVENT_CALLBACKS.clear
1654
-
1655
1606
 
1656
1607
  class Event(abc.ABC): # noqa
1657
- """Abstract event type """
1608
+ """Abstract event type."""
1658
1609
 
1659
1610
 
1660
1611
  class ProcessLogEvent(Event, abc.ABC):
@@ -1734,7 +1685,7 @@ class RemoteCommunicationEvent(Event):
1734
1685
 
1735
1686
 
1736
1687
  class SupervisorStateChangeEvent(Event):
1737
- """ Abstract class """
1688
+ """Abstract class."""
1738
1689
 
1739
1690
  def payload(self):
1740
1691
  return ''
@@ -1756,7 +1707,7 @@ class EventRejectedEvent: # purposely does not subclass Event
1756
1707
 
1757
1708
 
1758
1709
  class ProcessStateEvent(Event):
1759
- """ Abstract class, never raised directly """
1710
+ """Abstract class, never raised directly."""
1760
1711
  frm = None
1761
1712
  to = None
1762
1713
 
@@ -1845,7 +1796,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent):
1845
1796
 
1846
1797
 
1847
1798
  class TickEvent(Event):
1848
- """ Abstract """
1799
+ """Abstract."""
1849
1800
 
1850
1801
  def __init__(self, when, supervisord):
1851
1802
  super().__init__()
@@ -1888,39 +1839,273 @@ class EventTypes:
1888
1839
  PROCESS_STATE_RUNNING = ProcessStateRunningEvent
1889
1840
  PROCESS_STATE_UNKNOWN = ProcessStateUnknownEvent
1890
1841
 
1891
- PROCESS_COMMUNICATION = ProcessCommunicationEvent # abstract
1892
- PROCESS_COMMUNICATION_STDOUT = ProcessCommunicationStdoutEvent
1893
- PROCESS_COMMUNICATION_STDERR = ProcessCommunicationStderrEvent
1842
+ PROCESS_COMMUNICATION = ProcessCommunicationEvent # abstract
1843
+ PROCESS_COMMUNICATION_STDOUT = ProcessCommunicationStdoutEvent
1844
+ PROCESS_COMMUNICATION_STDERR = ProcessCommunicationStderrEvent
1845
+
1846
+ PROCESS_LOG = ProcessLogEvent
1847
+ PROCESS_LOG_STDOUT = ProcessLogStdoutEvent
1848
+ PROCESS_LOG_STDERR = ProcessLogStderrEvent
1849
+
1850
+ REMOTE_COMMUNICATION = RemoteCommunicationEvent
1851
+
1852
+ SUPERVISOR_STATE_CHANGE = SupervisorStateChangeEvent # abstract
1853
+ SUPERVISOR_STATE_CHANGE_RUNNING = SupervisorRunningEvent
1854
+ SUPERVISOR_STATE_CHANGE_STOPPING = SupervisorStoppingEvent
1855
+
1856
+ TICK = TickEvent # abstract
1857
+ TICK_5 = Tick5Event
1858
+ TICK_60 = Tick60Event
1859
+ TICK_3600 = Tick3600Event
1860
+
1861
+ PROCESS_GROUP = ProcessGroupEvent # abstract
1862
+ PROCESS_GROUP_ADDED = ProcessGroupAddedEvent
1863
+ PROCESS_GROUP_REMOVED = ProcessGroupRemovedEvent
1864
+
1865
+
1866
+ def get_event_name_by_type(requested):
1867
+ for name, typ in EventTypes.__dict__.items():
1868
+ if typ is requested:
1869
+ return name
1870
+ return None
1871
+
1872
+
1873
+ def register(name, event):
1874
+ setattr(EventTypes, name, event)
1875
+
1876
+
1877
+ ########################################
1878
+ # ../poller.py
1879
+
1880
+
1881
+ class BasePoller(abc.ABC):
1882
+
1883
+ def __init__(self) -> None:
1884
+ super().__init__()
1885
+
1886
+ @abc.abstractmethod
1887
+ def register_readable(self, fd: int) -> None:
1888
+ raise NotImplementedError
1889
+
1890
+ @abc.abstractmethod
1891
+ def register_writable(self, fd: int) -> None:
1892
+ raise NotImplementedError
1893
+
1894
+ @abc.abstractmethod
1895
+ def unregister_readable(self, fd: int) -> None:
1896
+ raise NotImplementedError
1897
+
1898
+ @abc.abstractmethod
1899
+ def unregister_writable(self, fd: int) -> None:
1900
+ raise NotImplementedError
1901
+
1902
+ @abc.abstractmethod
1903
+ def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
1904
+ raise NotImplementedError
1905
+
1906
+ def before_daemonize(self) -> None: # noqa
1907
+ pass
1908
+
1909
+ def after_daemonize(self) -> None: # noqa
1910
+ pass
1911
+
1912
+ def close(self) -> None: # noqa
1913
+ pass
1914
+
1915
+
1916
+ class SelectPoller(BasePoller):
1917
+
1918
+ def __init__(self) -> None:
1919
+ super().__init__()
1920
+
1921
+ self._readables: ta.Set[int] = set()
1922
+ self._writables: ta.Set[int] = set()
1923
+
1924
+ def register_readable(self, fd: int) -> None:
1925
+ self._readables.add(fd)
1926
+
1927
+ def register_writable(self, fd: int) -> None:
1928
+ self._writables.add(fd)
1929
+
1930
+ def unregister_readable(self, fd: int) -> None:
1931
+ self._readables.discard(fd)
1932
+
1933
+ def unregister_writable(self, fd: int) -> None:
1934
+ self._writables.discard(fd)
1935
+
1936
+ def unregister_all(self) -> None:
1937
+ self._readables.clear()
1938
+ self._writables.clear()
1939
+
1940
+ def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
1941
+ try:
1942
+ r, w, x = select.select(
1943
+ self._readables,
1944
+ self._writables,
1945
+ [], timeout,
1946
+ )
1947
+ except OSError as err:
1948
+ if err.args[0] == errno.EINTR:
1949
+ log.debug('EINTR encountered in poll')
1950
+ return [], []
1951
+ if err.args[0] == errno.EBADF:
1952
+ log.debug('EBADF encountered in poll')
1953
+ self.unregister_all()
1954
+ return [], []
1955
+ raise
1956
+ return r, w
1957
+
1958
+
1959
+ class PollPoller(BasePoller):
1960
+ _READ = select.POLLIN | select.POLLPRI | select.POLLHUP
1961
+ _WRITE = select.POLLOUT
1962
+
1963
+ def __init__(self) -> None:
1964
+ super().__init__()
1965
+
1966
+ self._poller = select.poll()
1967
+ self._readables: set[int] = set()
1968
+ self._writables: set[int] = set()
1969
+
1970
+ def register_readable(self, fd: int) -> None:
1971
+ self._poller.register(fd, self._READ)
1972
+ self._readables.add(fd)
1973
+
1974
+ def register_writable(self, fd: int) -> None:
1975
+ self._poller.register(fd, self._WRITE)
1976
+ self._writables.add(fd)
1977
+
1978
+ def unregister_readable(self, fd: int) -> None:
1979
+ self._readables.discard(fd)
1980
+ self._poller.unregister(fd)
1981
+ if fd in self._writables:
1982
+ self._poller.register(fd, self._WRITE)
1983
+
1984
+ def unregister_writable(self, fd: int) -> None:
1985
+ self._writables.discard(fd)
1986
+ self._poller.unregister(fd)
1987
+ if fd in self._readables:
1988
+ self._poller.register(fd, self._READ)
1989
+
1990
+ def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
1991
+ fds = self._poll_fds(timeout) # type: ignore
1992
+ readables, writables = [], []
1993
+ for fd, eventmask in fds:
1994
+ if self._ignore_invalid(fd, eventmask):
1995
+ continue
1996
+ if eventmask & self._READ:
1997
+ readables.append(fd)
1998
+ if eventmask & self._WRITE:
1999
+ writables.append(fd)
2000
+ return readables, writables
2001
+
2002
+ def _poll_fds(self, timeout: float) -> ta.List[ta.Tuple[int, int]]:
2003
+ try:
2004
+ return self._poller.poll(timeout * 1000)
2005
+ except OSError as err:
2006
+ if err.args[0] == errno.EINTR:
2007
+ log.debug('EINTR encountered in poll')
2008
+ return []
2009
+ raise
2010
+
2011
+ def _ignore_invalid(self, fd: int, eventmask: int) -> bool:
2012
+ if eventmask & select.POLLNVAL:
2013
+ # POLLNVAL means `fd` value is invalid, not open. When a process quits it's `fd`s are closed so there is no
2014
+ # more reason to keep this `fd` registered If the process restarts it's `fd`s are registered again.
2015
+ self._poller.unregister(fd)
2016
+ self._readables.discard(fd)
2017
+ self._writables.discard(fd)
2018
+ return True
2019
+ return False
2020
+
2021
+
2022
+ if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
2023
+ class KqueuePoller(BasePoller):
2024
+ max_events = 1000
2025
+
2026
+ def __init__(self) -> None:
2027
+ super().__init__()
2028
+
2029
+ self._kqueue: ta.Optional[ta.Any] = select.kqueue()
2030
+ self._readables: set[int] = set()
2031
+ self._writables: set[int] = set()
2032
+
2033
+ def register_readable(self, fd: int) -> None:
2034
+ self._readables.add(fd)
2035
+ kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD)
2036
+ self._kqueue_control(fd, kevent)
2037
+
2038
+ def register_writable(self, fd: int) -> None:
2039
+ self._writables.add(fd)
2040
+ kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_ADD)
2041
+ self._kqueue_control(fd, kevent)
2042
+
2043
+ def unregister_readable(self, fd: int) -> None:
2044
+ kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_DELETE)
2045
+ self._readables.discard(fd)
2046
+ self._kqueue_control(fd, kevent)
2047
+
2048
+ def unregister_writable(self, fd: int) -> None:
2049
+ kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_DELETE)
2050
+ self._writables.discard(fd)
2051
+ self._kqueue_control(fd, kevent)
2052
+
2053
+ def _kqueue_control(self, fd: int, kevent: 'select.kevent') -> None:
2054
+ try:
2055
+ self._kqueue.control([kevent], 0) # type: ignore
2056
+ except OSError as error:
2057
+ if error.errno == errno.EBADF:
2058
+ log.debug('EBADF encountered in kqueue. Invalid file descriptor %s', fd)
2059
+ else:
2060
+ raise
2061
+
2062
+ def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
2063
+ readables, writables = [], [] # type: ignore
1894
2064
 
1895
- PROCESS_LOG = ProcessLogEvent
1896
- PROCESS_LOG_STDOUT = ProcessLogStdoutEvent
1897
- PROCESS_LOG_STDERR = ProcessLogStderrEvent
2065
+ try:
2066
+ kevents = self._kqueue.control(None, self.max_events, timeout) # type: ignore
2067
+ except OSError as error:
2068
+ if error.errno == errno.EINTR:
2069
+ log.debug('EINTR encountered in poll')
2070
+ return readables, writables
2071
+ raise
1898
2072
 
1899
- REMOTE_COMMUNICATION = RemoteCommunicationEvent
2073
+ for kevent in kevents:
2074
+ if kevent.filter == select.KQ_FILTER_READ:
2075
+ readables.append(kevent.ident)
2076
+ if kevent.filter == select.KQ_FILTER_WRITE:
2077
+ writables.append(kevent.ident)
1900
2078
 
1901
- SUPERVISOR_STATE_CHANGE = SupervisorStateChangeEvent # abstract
1902
- SUPERVISOR_STATE_CHANGE_RUNNING = SupervisorRunningEvent
1903
- SUPERVISOR_STATE_CHANGE_STOPPING = SupervisorStoppingEvent
2079
+ return readables, writables
1904
2080
 
1905
- TICK = TickEvent # abstract
1906
- TICK_5 = Tick5Event
1907
- TICK_60 = Tick60Event
1908
- TICK_3600 = Tick3600Event
2081
+ def before_daemonize(self) -> None:
2082
+ self.close()
1909
2083
 
1910
- PROCESS_GROUP = ProcessGroupEvent # abstract
1911
- PROCESS_GROUP_ADDED = ProcessGroupAddedEvent
1912
- PROCESS_GROUP_REMOVED = ProcessGroupRemovedEvent
2084
+ def after_daemonize(self) -> None:
2085
+ self._kqueue = select.kqueue()
2086
+ for fd in self._readables:
2087
+ self.register_readable(fd)
2088
+ for fd in self._writables:
2089
+ self.register_writable(fd)
1913
2090
 
2091
+ def close(self) -> None:
2092
+ self._kqueue.close() # type: ignore
2093
+ self._kqueue = None
1914
2094
 
1915
- def get_event_name_by_type(requested):
1916
- for name, typ in EventTypes.__dict__.items():
1917
- if typ is requested:
1918
- return name
1919
- return None
2095
+ else:
2096
+ KqueuePoller = None
1920
2097
 
1921
2098
 
1922
- def register(name, event):
1923
- setattr(EventTypes, name, event)
2099
+ Poller: ta.Type[BasePoller]
2100
+ if (
2101
+ sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
2102
+ hasattr(select, 'kqueue') and KqueuePoller is not None
2103
+ ):
2104
+ Poller = KqueuePoller
2105
+ elif hasattr(select, 'poll'):
2106
+ Poller = PollPoller
2107
+ else:
2108
+ Poller = SelectPoller
1924
2109
 
1925
2110
 
1926
2111
  ########################################
@@ -1973,41 +2158,47 @@ class AbstractSubprocess(abc.ABC):
1973
2158
  # ../context.py
1974
2159
 
1975
2160
 
1976
- log = logging.getLogger(__name__)
1977
-
1978
-
1979
2161
  class ServerContext(AbstractServerContext):
1980
- first = False
1981
- test = False
1982
-
1983
- ##
1984
-
1985
- def __init__(self, config: ServerConfig) -> None:
2162
+ def __init__(
2163
+ self,
2164
+ config: ServerConfig,
2165
+ *,
2166
+ epoch: int = 0,
2167
+ ) -> None:
1986
2168
  super().__init__()
1987
2169
 
1988
2170
  self._config = config
2171
+ self._epoch = epoch
1989
2172
 
1990
2173
  self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
1991
2174
  self._state: SupervisorState = SupervisorStates.RUNNING
1992
2175
 
1993
- self.signal_receiver = SignalReceiver()
2176
+ self._signal_receiver = SignalReceiver()
1994
2177
 
1995
- self.poller = Poller()
2178
+ self._poller: BasePoller = Poller()
1996
2179
 
1997
- if self.config.user is not None:
1998
- uid = name_to_uid(self.config.user)
1999
- self.uid = uid
2000
- self.gid = gid_for_uid(uid)
2180
+ if config.user is not None:
2181
+ uid = name_to_uid(config.user)
2182
+ self._uid: ta.Optional[int] = uid
2183
+ self._gid: ta.Optional[int] = gid_for_uid(uid)
2001
2184
  else:
2002
- self.uid = None
2003
- self.gid = None
2185
+ self._uid = None
2186
+ self._gid = None
2004
2187
 
2005
- self.unlink_pidfile = False
2188
+ self._unlink_pidfile = False
2006
2189
 
2007
2190
  @property
2008
2191
  def config(self) -> ServerConfig:
2009
2192
  return self._config
2010
2193
 
2194
+ @property
2195
+ def epoch(self) -> int:
2196
+ return self._epoch
2197
+
2198
+ @property
2199
+ def first(self) -> bool:
2200
+ return not self._epoch
2201
+
2011
2202
  @property
2012
2203
  def state(self) -> SupervisorState:
2013
2204
  return self._state
@@ -2015,17 +2206,26 @@ class ServerContext(AbstractServerContext):
2015
2206
  def set_state(self, state: SupervisorState) -> None:
2016
2207
  self._state = state
2017
2208
 
2209
+ @property
2210
+ def poller(self) -> BasePoller:
2211
+ return self._poller
2212
+
2018
2213
  @property
2019
2214
  def pid_history(self) -> ta.Dict[int, AbstractSubprocess]:
2020
2215
  return self._pid_history
2021
2216
 
2022
- uid: ta.Optional[int]
2023
- gid: ta.Optional[int]
2217
+ @property
2218
+ def uid(self) -> ta.Optional[int]:
2219
+ return self._uid
2220
+
2221
+ @property
2222
+ def gid(self) -> ta.Optional[int]:
2223
+ return self._gid
2024
2224
 
2025
2225
  ##
2026
2226
 
2027
2227
  def set_signals(self) -> None:
2028
- self.signal_receiver.install(
2228
+ self._signal_receiver.install(
2029
2229
  signal.SIGTERM,
2030
2230
  signal.SIGINT,
2031
2231
  signal.SIGQUIT,
@@ -2136,7 +2336,7 @@ class ServerContext(AbstractServerContext):
2136
2336
  ))
2137
2337
 
2138
2338
  def cleanup(self) -> None:
2139
- if self.unlink_pidfile:
2339
+ if self._unlink_pidfile:
2140
2340
  try_unlink(self.config.pidfile)
2141
2341
  self.poller.close()
2142
2342
 
@@ -2189,6 +2389,7 @@ class ServerContext(AbstractServerContext):
2189
2389
  # Parent
2190
2390
  log.debug('supervisord forked; parent exiting')
2191
2391
  real_exit(0)
2392
+
2192
2393
  # Child
2193
2394
  log.info('daemonizing the supervisord process')
2194
2395
  if self.config.directory:
@@ -2198,11 +2399,15 @@ class ServerContext(AbstractServerContext):
2198
2399
  log.critical("can't chdir into %r: %s", self.config.directory, err)
2199
2400
  else:
2200
2401
  log.info('set current directory: %r', self.config.directory)
2402
+
2201
2403
  os.dup2(0, os.open('/dev/null', os.O_RDONLY))
2202
2404
  os.dup2(1, os.open('/dev/null', os.O_WRONLY))
2203
2405
  os.dup2(2, os.open('/dev/null', os.O_WRONLY))
2406
+
2204
2407
  os.setsid()
2408
+
2205
2409
  os.umask(self.config.umask)
2410
+
2206
2411
  # XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
2207
2412
  # file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
2208
2413
  # again after the setsid() call, for obscure SVR4 reasons.
@@ -2217,7 +2422,7 @@ class ServerContext(AbstractServerContext):
2217
2422
  return logfile
2218
2423
 
2219
2424
  def get_signal(self) -> ta.Optional[int]:
2220
- return self.signal_receiver.get_signal()
2425
+ return self._signal_receiver.get_signal()
2221
2426
 
2222
2427
  def write_pidfile(self) -> None:
2223
2428
  pid = os.getpid()
@@ -2227,7 +2432,7 @@ class ServerContext(AbstractServerContext):
2227
2432
  except OSError:
2228
2433
  log.critical('could not write pidfile %s', self.config.pidfile)
2229
2434
  else:
2230
- self.unlink_pidfile = True
2435
+ self._unlink_pidfile = True
2231
2436
  log.info('supervisord started with pid %s', pid)
2232
2437
 
2233
2438
 
@@ -2280,11 +2485,14 @@ def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
2280
2485
  os.setgroups(groups)
2281
2486
  except OSError:
2282
2487
  return 'Could not set groups of effective user'
2488
+
2283
2489
  try:
2284
2490
  os.setgid(gid)
2285
2491
  except OSError:
2286
2492
  return 'Could not set group id of effective user'
2493
+
2287
2494
  os.setuid(uid)
2495
+
2288
2496
  return None
2289
2497
 
2290
2498
 
@@ -2354,9 +2562,6 @@ def check_execv_args(filename, argv, st) -> None:
2354
2562
  # ../dispatchers.py
2355
2563
 
2356
2564
 
2357
- log = logging.getLogger(__name__)
2358
-
2359
-
2360
2565
  class Dispatcher(abc.ABC):
2361
2566
 
2362
2567
  def __init__(self, process: AbstractSubprocess, channel: str, fd: int) -> None:
@@ -2513,16 +2718,16 @@ class OutputDispatcher(Dispatcher):
2513
2718
  # )
2514
2719
 
2515
2720
  def remove_logs(self):
2516
- for log in (self._normal_log, self._capture_log):
2517
- if log is not None:
2518
- for handler in log.handlers:
2721
+ for l in (self._normal_log, self._capture_log):
2722
+ if l is not None:
2723
+ for handler in l.handlers:
2519
2724
  handler.remove() # type: ignore
2520
2725
  handler.reopen() # type: ignore
2521
2726
 
2522
2727
  def reopen_logs(self):
2523
- for log in (self._normal_log, self._capture_log):
2524
- if log is not None:
2525
- for handler in log.handlers:
2728
+ for l in (self._normal_log, self._capture_log):
2729
+ if l is not None:
2730
+ for handler in l.handlers:
2526
2731
  handler.reopen() # type: ignore
2527
2732
 
2528
2733
  def _log(self, data):
@@ -2545,10 +2750,10 @@ class OutputDispatcher(Dispatcher):
2545
2750
 
2546
2751
  if self._channel == 'stdout':
2547
2752
  if self._stdout_events_enabled:
2548
- notify_event(ProcessLogStdoutEvent(self._process, self._process.pid, data))
2753
+ EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
2549
2754
 
2550
2755
  elif self._stderr_events_enabled:
2551
- notify_event(ProcessLogStderrEvent(self._process, self._process.pid, data))
2756
+ EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
2552
2757
 
2553
2758
  def record_output(self):
2554
2759
  if self._capture_log is None:
@@ -2599,7 +2804,7 @@ class OutputDispatcher(Dispatcher):
2599
2804
  channel = self._channel
2600
2805
  procname = self._process.config.name
2601
2806
  event = self.event_type(self._process, self._process.pid, data)
2602
- notify_event(event)
2807
+ EVENT_CALLBACKS.notify(event)
2603
2808
 
2604
2809
  log.debug('%r %s emitted a comm event', procname, channel)
2605
2810
  for handler in self._capture_log.handlers:
@@ -2660,9 +2865,6 @@ class InputDispatcher(Dispatcher):
2660
2865
  # ../process.py
2661
2866
 
2662
2867
 
2663
- log = logging.getLogger(__name__)
2664
-
2665
-
2666
2868
  @functools.total_ordering
2667
2869
  class Subprocess(AbstractSubprocess):
2668
2870
  """A class to manage a subprocess."""
@@ -2815,7 +3017,7 @@ class Subprocess(AbstractSubprocess):
2815
3017
  event_class = self.event_map.get(new_state)
2816
3018
  if event_class is not None:
2817
3019
  event = event_class(self, old_state, expected)
2818
- notify_event(event)
3020
+ EVENT_CALLBACKS.notify(event)
2819
3021
 
2820
3022
  return True
2821
3023
 
@@ -2932,6 +3134,7 @@ class Subprocess(AbstractSubprocess):
2932
3134
  os.dup2(self._pipes['child_stdout'], 2)
2933
3135
  else:
2934
3136
  os.dup2(self._pipes['child_stderr'], 2)
3137
+ # FIXME: leave debugger fds
2935
3138
  for i in range(3, self.context.config.minfds):
2936
3139
  close_fd(i)
2937
3140
 
@@ -2967,7 +3170,7 @@ class Subprocess(AbstractSubprocess):
2967
3170
  cwd = self.config.directory
2968
3171
  try:
2969
3172
  if cwd is not None:
2970
- os.chdir(cwd)
3173
+ os.chdir(os.path.expanduser(cwd))
2971
3174
  except OSError as why:
2972
3175
  code = errno.errorcode.get(why.args[0], why.args[0])
2973
3176
  msg = f"couldn't chdir to {cwd}: {code}\n"
@@ -3023,7 +3226,7 @@ class Subprocess(AbstractSubprocess):
3023
3226
  return self.kill(self.config.stopsignal)
3024
3227
 
3025
3228
  def stop_report(self) -> None:
3026
- """ Log a 'waiting for x to stop' message with throttling. """
3229
+ """Log a 'waiting for x to stop' message with throttling."""
3027
3230
  if self.state == ProcessStates.STOPPING:
3028
3231
  now = time.time()
3029
3232
 
@@ -3153,7 +3356,7 @@ class Subprocess(AbstractSubprocess):
3153
3356
  return None
3154
3357
 
3155
3358
  def finish(self, sts: int) -> None:
3156
- """ The process was reaped and we need to report and manage its state """
3359
+ """The process was reaped and we need to report and manage its state."""
3157
3360
 
3158
3361
  self.drain()
3159
3362
 
@@ -3234,7 +3437,7 @@ class Subprocess(AbstractSubprocess):
3234
3437
  # system that this event was rejected so it can be processed again.
3235
3438
  if self.event is not None:
3236
3439
  # Note: this should only be true if we were in the BUSY state when finish() was called.
3237
- notify_event(EventRejectedEvent(self, self.event)) # type: ignore
3440
+ EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
3238
3441
  self.event = None
3239
3442
 
3240
3443
  def set_uid(self) -> ta.Optional[str]:
@@ -3397,10 +3600,11 @@ class ProcessGroup:
3397
3600
 
3398
3601
 
3399
3602
  ########################################
3400
- # supervisor.py
3603
+ # ../supervisor.py
3401
3604
 
3402
3605
 
3403
- log = logging.getLogger(__name__)
3606
+ def timeslice(period: int, when: float) -> int:
3607
+ return int(when - (when % period))
3404
3608
 
3405
3609
 
3406
3610
  class Supervisor:
@@ -3415,6 +3619,8 @@ class Supervisor:
3415
3619
  self._stopping = False # set after we detect that we are handling a stop request
3416
3620
  self._last_shutdown_report = 0. # throttle for delayed process error reports at stop
3417
3621
 
3622
+ #
3623
+
3418
3624
  @property
3419
3625
  def context(self) -> ServerContext:
3420
3626
  return self._context
@@ -3422,47 +3628,14 @@ class Supervisor:
3422
3628
  def get_state(self) -> SupervisorState:
3423
3629
  return self._context.state
3424
3630
 
3425
- def main(self) -> None:
3426
- if not self._context.first:
3427
- # prevent crash on libdispatch-based systems, at least for the first request
3428
- self._context.cleanup_fds()
3429
-
3430
- self._context.set_uid_or_exit()
3431
-
3432
- if self._context.first:
3433
- self._context.set_rlimits_or_exit()
3434
-
3435
- # this sets the options.logger object delay logger instantiation until after setuid
3436
- if not self._context.config.nocleanup:
3437
- # clean up old automatic logs
3438
- self._context.clear_auto_child_logdir()
3439
-
3440
- self.run()
3441
-
3442
- def run(self) -> None:
3443
- self._process_groups = {} # clear
3444
- self._stop_groups = None # clear
3445
-
3446
- clear_events()
3447
-
3448
- try:
3449
- for config in self._context.config.groups or []:
3450
- self.add_process_group(config)
3451
-
3452
- self._context.set_signals()
3453
-
3454
- if not self._context.config.nodaemon and self._context.first:
3455
- self._context.daemonize()
3456
-
3457
- # writing pid file needs to come *after* daemonizing or pid will be wrong
3458
- self._context.write_pidfile()
3459
-
3460
- self.runforever()
3631
+ #
3461
3632
 
3462
- finally:
3463
- self._context.cleanup()
3633
+ class DiffToActive(ta.NamedTuple):
3634
+ added: ta.List[ProcessGroupConfig]
3635
+ changed: ta.List[ProcessGroupConfig]
3636
+ removed: ta.List[ProcessGroupConfig]
3464
3637
 
3465
- def diff_to_active(self):
3638
+ def diff_to_active(self) -> DiffToActive:
3466
3639
  new = self._context.config.groups or []
3467
3640
  cur = [group.config for group in self._process_groups.values()]
3468
3641
 
@@ -3474,7 +3647,7 @@ class Supervisor:
3474
3647
 
3475
3648
  changed = [cand for cand in new if cand != curdict.get(cand.name, cand)]
3476
3649
 
3477
- return added, changed, removed
3650
+ return Supervisor.DiffToActive(added, changed, removed)
3478
3651
 
3479
3652
  def add_process_group(self, config: ProcessGroupConfig) -> bool:
3480
3653
  name = config.name
@@ -3484,7 +3657,7 @@ class Supervisor:
3484
3657
  group = self._process_groups[name] = ProcessGroup(config, self._context)
3485
3658
  group.after_setuid()
3486
3659
 
3487
- notify_event(ProcessGroupAddedEvent(name))
3660
+ EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
3488
3661
  return True
3489
3662
 
3490
3663
  def remove_process_group(self, name: str) -> bool:
@@ -3495,7 +3668,7 @@ class Supervisor:
3495
3668
 
3496
3669
  del self._process_groups[name]
3497
3670
 
3498
- notify_event(ProcessGroupRemovedEvent(name))
3671
+ EVENT_CALLBACKS.notify(ProcessGroupRemovedEvent(name))
3499
3672
  return True
3500
3673
 
3501
3674
  def get_process_map(self) -> ta.Dict[int, Dispatcher]:
@@ -3524,6 +3697,72 @@ class Supervisor:
3524
3697
 
3525
3698
  return unstopped
3526
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
+
3527
3766
  def _ordered_stop_groups_phase_1(self) -> None:
3528
3767
  if self._stop_groups:
3529
3768
  # stop the last group (the one with the "highest" priority)
@@ -3540,110 +3779,77 @@ class Supervisor:
3540
3779
  # down, so push it back on to the end of the stop group queue
3541
3780
  self._stop_groups.append(group)
3542
3781
 
3543
- def runforever(self) -> None:
3544
- notify_event(SupervisorRunningEvent())
3545
- timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
3546
-
3547
- while True:
3548
- combined_map = {}
3549
- combined_map.update(self.get_process_map())
3782
+ def _poll(self) -> None:
3783
+ combined_map = {}
3784
+ combined_map.update(self.get_process_map())
3550
3785
 
3551
- pgroups = list(self._process_groups.values())
3552
- pgroups.sort()
3786
+ pgroups = list(self._process_groups.values())
3787
+ pgroups.sort()
3553
3788
 
3554
- if self._context.state < SupervisorStates.RUNNING:
3555
- if not self._stopping:
3556
- # first time, set the stopping flag, do a notification and set stop_groups
3557
- self._stopping = True
3558
- self._stop_groups = pgroups[:]
3559
- notify_event(SupervisorStoppingEvent())
3789
+ if self._context.state < SupervisorStates.RUNNING:
3790
+ if not self._stopping:
3791
+ # first time, set the stopping flag, do a notification and set stop_groups
3792
+ self._stopping = True
3793
+ self._stop_groups = pgroups[:]
3794
+ EVENT_CALLBACKS.notify(SupervisorStoppingEvent())
3560
3795
 
3561
- self._ordered_stop_groups_phase_1()
3796
+ self._ordered_stop_groups_phase_1()
3562
3797
 
3563
- if not self.shutdown_report():
3564
- # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
3565
- raise ExitNow
3798
+ if not self.shutdown_report():
3799
+ # if there are no unstopped processes (we're done killing everything), it's OK to shutdown or reload
3800
+ raise ExitNow
3566
3801
 
3567
- for fd, dispatcher in combined_map.items():
3568
- if dispatcher.readable():
3569
- self._context.poller.register_readable(fd)
3570
- if dispatcher.writable():
3571
- self._context.poller.register_writable(fd)
3802
+ for fd, dispatcher in combined_map.items():
3803
+ if dispatcher.readable():
3804
+ self._context.poller.register_readable(fd)
3805
+ if dispatcher.writable():
3806
+ self._context.poller.register_writable(fd)
3572
3807
 
3573
- r, w = self._context.poller.poll(timeout)
3808
+ timeout = 1 # this cannot be fewer than the smallest TickEvent (5)
3809
+ r, w = self._context.poller.poll(timeout)
3574
3810
 
3575
- for fd in r:
3576
- if fd in combined_map:
3577
- try:
3578
- dispatcher = combined_map[fd]
3579
- log.debug('read event caused by %r', dispatcher)
3580
- dispatcher.handle_read_event()
3581
- if not dispatcher.readable():
3582
- self._context.poller.unregister_readable(fd)
3583
- except ExitNow:
3584
- raise
3585
- except Exception: # noqa
3586
- combined_map[fd].handle_error()
3587
- else:
3588
- # if the fd is not in combined_map, we should unregister it. otherwise, it will be polled every
3589
- # time, which may cause 100% cpu usage
3590
- log.debug('unexpected read event from fd %r', fd)
3591
- try:
3811
+ for fd in r:
3812
+ if fd in combined_map:
3813
+ try:
3814
+ dispatcher = combined_map[fd]
3815
+ log.debug('read event caused by %r', dispatcher)
3816
+ dispatcher.handle_read_event()
3817
+ if not dispatcher.readable():
3592
3818
  self._context.poller.unregister_readable(fd)
3593
- except Exception: # noqa
3594
- pass
3819
+ except ExitNow:
3820
+ raise
3821
+ except Exception: # noqa
3822
+ combined_map[fd].handle_error()
3823
+ else:
3824
+ # if the fd is not in combined_map, we should unregister it. otherwise, it will be polled every
3825
+ # time, which may cause 100% cpu usage
3826
+ log.debug('unexpected read event from fd %r', fd)
3827
+ try:
3828
+ self._context.poller.unregister_readable(fd)
3829
+ except Exception: # noqa
3830
+ pass
3595
3831
 
3596
- for fd in w:
3597
- if fd in combined_map:
3598
- try:
3599
- dispatcher = combined_map[fd]
3600
- log.debug('write event caused by %r', dispatcher)
3601
- dispatcher.handle_write_event()
3602
- if not dispatcher.writable():
3603
- self._context.poller.unregister_writable(fd)
3604
- except ExitNow:
3605
- raise
3606
- except Exception: # noqa
3607
- combined_map[fd].handle_error()
3608
- else:
3609
- log.debug('unexpected write event from fd %r', fd)
3610
- try:
3832
+ for fd in w:
3833
+ if fd in combined_map:
3834
+ try:
3835
+ dispatcher = combined_map[fd]
3836
+ log.debug('write event caused by %r', dispatcher)
3837
+ dispatcher.handle_write_event()
3838
+ if not dispatcher.writable():
3611
3839
  self._context.poller.unregister_writable(fd)
3612
- except Exception: # noqa
3613
- pass
3614
-
3615
- for group in pgroups:
3616
- group.transition()
3617
-
3618
- self._reap()
3619
- self._handle_signal()
3620
- self._tick()
3621
-
3622
- if self._context.state < SupervisorStates.RUNNING:
3623
- self._ordered_stop_groups_phase_2()
3624
-
3625
- if self._context.test:
3626
- break
3627
-
3628
- def _tick(self, now: ta.Optional[float] = None) -> None:
3629
- """Send one or more 'tick' events when the timeslice related to the period for the event type rolls over"""
3630
-
3631
- if now is None:
3632
- # now won't be None in unit tests
3633
- now = time.time()
3634
-
3635
- for event in TICK_EVENTS:
3636
- period = event.period # type: ignore
3637
-
3638
- last_tick = self._ticks.get(period)
3639
- if last_tick is None:
3640
- # we just started up
3641
- last_tick = self._ticks[period] = timeslice(period, now)
3840
+ except ExitNow:
3841
+ raise
3842
+ except Exception: # noqa
3843
+ combined_map[fd].handle_error()
3844
+ else:
3845
+ log.debug('unexpected write event from fd %r', fd)
3846
+ try:
3847
+ self._context.poller.unregister_writable(fd)
3848
+ except Exception: # noqa
3849
+ pass
3642
3850
 
3643
- this_tick = timeslice(period, now)
3644
- if this_tick != last_tick:
3645
- self._ticks[period] = this_tick
3646
- notify_event(event(this_tick, self))
3851
+ for group in pgroups:
3852
+ group.transition()
3647
3853
 
3648
3854
  def _reap(self, *, once: bool = False, depth: int = 0) -> None:
3649
3855
  if depth >= 100:
@@ -3693,50 +3899,77 @@ class Supervisor:
3693
3899
  else:
3694
3900
  log.debug('received %s indicating nothing', signame(sig))
3695
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()
3696
3908
 
3697
- def timeslice(period, when):
3698
- return int(when - (when % period))
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
+
3922
+
3923
+ ########################################
3924
+ # main.py
3699
3925
 
3700
3926
 
3701
- def main(args=None, test=False):
3927
+ def main(
3928
+ argv: ta.Optional[ta.Sequence[str]] = None,
3929
+ *,
3930
+ no_logging: bool = False,
3931
+ ) -> None:
3702
3932
  import argparse
3703
3933
 
3704
3934
  parser = argparse.ArgumentParser()
3705
3935
  parser.add_argument('config_file', metavar='config-file')
3706
- args = parser.parse_args()
3936
+ parser.add_argument('--no-journald', action='store_true')
3937
+ args = parser.parse_args(argv)
3707
3938
 
3708
- configure_standard_logging('INFO')
3939
+ #
3709
3940
 
3710
3941
  if not (cf := args.config_file):
3711
3942
  raise RuntimeError('No config file specified')
3712
3943
 
3944
+ if not no_logging:
3945
+ configure_standard_logging(
3946
+ 'INFO',
3947
+ handler_factory=journald_log_handler_factory if not args.no_journald else None,
3948
+ )
3949
+
3950
+ #
3951
+
3713
3952
  # if we hup, restart by making a new Supervisor()
3714
- first = True
3715
- while True:
3953
+ for epoch in itertools.count():
3716
3954
  with open(cf) as f:
3717
3955
  config_src = f.read()
3956
+
3718
3957
  config_dct = json.loads(config_src)
3719
3958
  config: ServerConfig = unmarshal_obj(config_dct, ServerConfig)
3720
3959
 
3721
3960
  context = ServerContext(
3722
3961
  config,
3962
+ epoch=epoch,
3723
3963
  )
3724
3964
 
3725
- context.first = first
3726
- context.test = test
3727
- go(context)
3728
- # options.close_logger()
3729
- first = False
3730
- if test or (context.state < SupervisorStates.RESTARTING):
3731
- break
3732
-
3965
+ supervisor = Supervisor(context)
3966
+ try:
3967
+ supervisor.main()
3968
+ except ExitNow:
3969
+ pass
3733
3970
 
3734
- def go(context): # pragma: no cover
3735
- d = Supervisor(context)
3736
- try:
3737
- d.main()
3738
- except ExitNow:
3739
- pass
3971
+ if context.state < SupervisorStates.RESTARTING:
3972
+ break
3740
3973
 
3741
3974
 
3742
3975
  if __name__ == '__main__':