ominfra 0.0.0.dev193__py3-none-any.whl → 0.0.0.dev195__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.
@@ -36,6 +36,7 @@ import abc
36
36
  import base64
37
37
  import collections
38
38
  import collections.abc
39
+ import configparser
39
40
  import contextlib
40
41
  import contextvars
41
42
  import ctypes as ct
@@ -79,7 +80,7 @@ import types
79
80
  import typing as ta
80
81
  import uuid
81
82
  import warnings
82
- import weakref # noqa
83
+ import weakref
83
84
 
84
85
 
85
86
  ########################################
@@ -96,6 +97,12 @@ if sys.version_info < (3, 8):
96
97
  K = ta.TypeVar('K')
97
98
  V = ta.TypeVar('V')
98
99
 
100
+ # ../../omlish/configs/types.py
101
+ ConfigMap = ta.Mapping[str, ta.Any]
102
+
103
+ # ../../omlish/formats/ini/sections.py
104
+ IniSectionSettingsMap = ta.Mapping[str, ta.Mapping[str, ta.Union[str, ta.Sequence[str]]]] # ta.TypeAlias
105
+
99
106
  # ../../omlish/formats/toml/parser.py
100
107
  TomlParseFloat = ta.Callable[[str], ta.Any]
101
108
  TomlKey = ta.Tuple[str, ...]
@@ -125,6 +132,9 @@ SocketAddress = ta.Any
125
132
  EventCallback = ta.Callable[['Event'], None]
126
133
  ProcessOutputChannel = ta.Literal['stdout', 'stderr'] # ta.TypeAlias
127
134
 
135
+ # ../../omlish/configs/formats.py
136
+ ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
137
+
128
138
  # ../../omlish/http/parsing.py
129
139
  HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
130
140
 
@@ -141,9 +151,6 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
141
151
  # ../../omlish/sockets/handlers.py
142
152
  SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'SocketHandler']
143
153
 
144
- # ../configs.py
145
- ConfigMapping = ta.Mapping[str, ta.Any]
146
-
147
154
  # ../../omlish/http/handlers.py
148
155
  HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse'] # ta.TypeAlias
149
156
 
@@ -621,6 +628,54 @@ def parse_octal(arg: ta.Union[str, int]) -> int:
621
628
  raise ValueError(f'{arg} can not be converted to an octal type') # noqa
622
629
 
623
630
 
631
+ ########################################
632
+ # ../../../omlish/configs/types.py
633
+
634
+
635
+ #
636
+
637
+
638
+ ########################################
639
+ # ../../../omlish/formats/ini/sections.py
640
+
641
+
642
+ ##
643
+
644
+
645
+ def extract_ini_sections(cp: configparser.ConfigParser) -> IniSectionSettingsMap:
646
+ config_dct: ta.Dict[str, ta.Any] = {}
647
+ for sec in cp.sections():
648
+ cd = config_dct
649
+ for k in sec.split('.'):
650
+ cd = cd.setdefault(k, {})
651
+ cd.update(cp.items(sec))
652
+ return config_dct
653
+
654
+
655
+ ##
656
+
657
+
658
+ def render_ini_sections(
659
+ settings_by_section: IniSectionSettingsMap,
660
+ ) -> str:
661
+ out = io.StringIO()
662
+
663
+ for i, (section, settings) in enumerate(settings_by_section.items()):
664
+ if i:
665
+ out.write('\n')
666
+
667
+ out.write(f'[{section}]\n')
668
+
669
+ for k, v in settings.items():
670
+ if isinstance(v, str):
671
+ out.write(f'{k}={v}\n')
672
+ else:
673
+ for vv in v:
674
+ out.write(f'{k}={vv}\n')
675
+
676
+ return out.getvalue()
677
+
678
+
624
679
  ########################################
625
680
  # ../../../omlish/formats/toml/parser.py
626
681
  # SPDX-License-Identifier: MIT
@@ -1439,6 +1494,129 @@ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
1439
1494
  return safe_parse_float
1440
1495
 
1441
1496
 
1497
+ ########################################
1498
+ # ../../../omlish/formats/toml/writer.py
1499
+
1500
+
1501
+ class TomlWriter:
1502
+ @dc.dataclass(frozen=True)
1503
+ class Literal:
1504
+ s: str
1505
+
1506
+ def __init__(self, out: ta.TextIO) -> None:
1507
+ super().__init__()
1508
+ self._out = out
1509
+
1510
+ self._indent = 0
1511
+ self._wrote_indent = False
1512
+
1513
+ #
1514
+
1515
+ def _w(self, s: str) -> None:
1516
+ if not self._wrote_indent:
1517
+ self._out.write(' ' * self._indent)
1518
+ self._wrote_indent = True
1519
+ self._out.write(s)
1520
+
1521
+ def _nl(self) -> None:
1522
+ self._out.write('\n')
1523
+ self._wrote_indent = False
1524
+
1525
+ def _needs_quote(self, s: str) -> bool:
1526
+ return (
1527
+ not s or
1528
+ any(c in s for c in '\'"\n') or
1529
+ s[0] not in string.ascii_letters
1530
+ )
1531
+
1532
+ def _maybe_quote(self, s: str) -> str:
1533
+ if self._needs_quote(s):
1534
+ return repr(s)
1535
+ else:
1536
+ return s
1537
+
1538
+ #
1539
+
1540
+ def write_root(self, obj: ta.Mapping) -> None:
1541
+ for i, (k, v) in enumerate(obj.items()):
1542
+ if i:
1543
+ self._nl()
1544
+ self._w('[')
1545
+ self._w(self._maybe_quote(k))
1546
+ self._w(']')
1547
+ self._nl()
1548
+ self.write_table_contents(v)
1549
+
1550
+ def write_table_contents(self, obj: ta.Mapping) -> None:
1551
+ for k, v in obj.items():
1552
+ self.write_key(k)
1553
+ self._w(' = ')
1554
+ self.write_value(v)
1555
+ self._nl()
1556
+
1557
+ def write_array(self, obj: ta.Sequence) -> None:
1558
+ self._w('[')
1559
+ self._nl()
1560
+ self._indent += 1
1561
+ for e in obj:
1562
+ self.write_value(e)
1563
+ self._w(',')
1564
+ self._nl()
1565
+ self._indent -= 1
1566
+ self._w(']')
1567
+
1568
+ def write_inline_table(self, obj: ta.Mapping) -> None:
1569
+ self._w('{')
1570
+ for i, (k, v) in enumerate(obj.items()):
1571
+ if i:
1572
+ self._w(', ')
1573
+ self.write_key(k)
1574
+ self._w(' = ')
1575
+ self.write_value(v)
1576
+ self._w('}')
1577
+
1578
+ def write_inline_array(self, obj: ta.Sequence) -> None:
1579
+ self._w('[')
1580
+ for i, e in enumerate(obj):
1581
+ if i:
1582
+ self._w(', ')
1583
+ self.write_value(e)
1584
+ self._w(']')
1585
+
1586
+ def write_key(self, obj: ta.Any) -> None:
1587
+ if isinstance(obj, TomlWriter.Literal):
1588
+ self._w(obj.s)
1589
+ elif isinstance(obj, str):
1590
+ self._w(self._maybe_quote(obj.replace('_', '-')))
1591
+ elif isinstance(obj, int):
1592
+ self._w(repr(str(obj)))
1593
+ else:
1594
+ raise TypeError(obj)
1595
+
1596
+ def write_value(self, obj: ta.Any) -> None:
1597
+ if isinstance(obj, bool):
1598
+ self._w(str(obj).lower())
1599
+ elif isinstance(obj, (str, int, float)):
1600
+ self._w(repr(obj))
1601
+ elif isinstance(obj, ta.Mapping):
1602
+ self.write_inline_table(obj)
1603
+ elif isinstance(obj, ta.Sequence):
1604
+ if not obj:
1605
+ self.write_inline_array(obj)
1606
+ else:
1607
+ self.write_array(obj)
1608
+ else:
1609
+ raise TypeError(obj)
1610
+
1611
+ #
1612
+
1613
+ @classmethod
1614
+ def write_str(cls, obj: ta.Any) -> str:
1615
+ out = io.StringIO()
1616
+ cls(out).write_value(obj)
1617
+ return out.getvalue()
1618
+
1619
+
1442
1620
  ########################################
1443
1621
  # ../../../omlish/http/versions.py
1444
1622
 
@@ -3125,6 +3303,283 @@ def get_user(name: str) -> User:
3125
3303
  )
3126
3304
 
3127
3305
 
3306
+ ########################################
3307
+ # ../../../omlish/configs/formats.py
3308
+ """
3309
+ Notes:
3310
+ - necessarily string-oriented
3311
+ - single file, as this is intended to be amalg'd and thus all included anyway
3312
+
3313
+ TODO:
3314
+ - ConfigDataMapper? to_map -> ConfigMap?
3315
+ - nginx ?
3316
+ - raw ?
3317
+ """
3318
+
3319
+
3320
+ ##
3321
+
3322
+
3323
+ @dc.dataclass(frozen=True)
3324
+ class ConfigData(abc.ABC): # noqa
3325
+ @abc.abstractmethod
3326
+ def as_map(self) -> ConfigMap:
3327
+ raise NotImplementedError
3328
+
3329
+
3330
+ #
3331
+
3332
+
3333
+ class ConfigLoader(abc.ABC, ta.Generic[ConfigDataT]):
3334
+ @property
3335
+ def file_exts(self) -> ta.Sequence[str]:
3336
+ return ()
3337
+
3338
+ def match_file(self, n: str) -> bool:
3339
+ return '.' in n and n.split('.')[-1] in check.not_isinstance(self.file_exts, str)
3340
+
3341
+ #
3342
+
3343
+ def load_file(self, p: str) -> ConfigDataT:
3344
+ with open(p) as f:
3345
+ return self.load_str(f.read())
3346
+
3347
+ @abc.abstractmethod
3348
+ def load_str(self, s: str) -> ConfigDataT:
3349
+ raise NotImplementedError
3350
+
3351
+
3352
+ #
3353
+
3354
+
3355
+ class ConfigRenderer(abc.ABC, ta.Generic[ConfigDataT]):
3356
+ @property
3357
+ @abc.abstractmethod
3358
+ def data_cls(self) -> ta.Type[ConfigDataT]:
3359
+ raise NotImplementedError
3360
+
3361
+ def match_data(self, d: ConfigDataT) -> bool:
3362
+ return isinstance(d, self.data_cls)
3363
+
3364
+ #
3365
+
3366
+ @abc.abstractmethod
3367
+ def render(self, d: ConfigDataT) -> str:
3368
+ raise NotImplementedError
3369
+
3370
+
3371
+ ##
3372
+
3373
+
3374
+ @dc.dataclass(frozen=True)
3375
+ class ObjConfigData(ConfigData, abc.ABC):
3376
+ obj: ta.Any
3377
+
3378
+ def as_map(self) -> ConfigMap:
3379
+ return check.isinstance(self.obj, collections.abc.Mapping)
3380
+
3381
+
3382
+ ##
3383
+
3384
+
3385
+ @dc.dataclass(frozen=True)
3386
+ class JsonConfigData(ObjConfigData):
3387
+ pass
3388
+
3389
+
3390
+ class JsonConfigLoader(ConfigLoader[JsonConfigData]):
3391
+ file_exts = ('json',)
3392
+
3393
+ def load_str(self, s: str) -> JsonConfigData:
3394
+ return JsonConfigData(json.loads(s))
3395
+
3396
+
3397
+ class JsonConfigRenderer(ConfigRenderer[JsonConfigData]):
3398
+ data_cls = JsonConfigData
3399
+
3400
+ def render(self, d: JsonConfigData) -> str:
3401
+ return json_dumps_pretty(d.obj)
3402
+
3403
+
3404
+ ##
3405
+
3406
+
3407
+ @dc.dataclass(frozen=True)
3408
+ class TomlConfigData(ObjConfigData):
3409
+ pass
3410
+
3411
+
3412
+ class TomlConfigLoader(ConfigLoader[TomlConfigData]):
3413
+ file_exts = ('toml',)
3414
+
3415
+ def load_str(self, s: str) -> TomlConfigData:
3416
+ return TomlConfigData(toml_loads(s))
3417
+
3418
+
3419
+ class TomlConfigRenderer(ConfigRenderer[TomlConfigData]):
3420
+ data_cls = TomlConfigData
3421
+
3422
+ def render(self, d: TomlConfigData) -> str:
3423
+ return TomlWriter.write_str(d.obj)
3424
+
3425
+
3426
+ ##
3427
+
3428
+
3429
+ @dc.dataclass(frozen=True)
3430
+ class YamlConfigData(ObjConfigData):
3431
+ pass
3432
+
3433
+
3434
+ class YamlConfigLoader(ConfigLoader[YamlConfigData]):
3435
+ file_exts = ('yaml', 'yml')
3436
+
3437
+ def load_str(self, s: str) -> YamlConfigData:
3438
+ return YamlConfigData(__import__('yaml').safe_load(s))
3439
+
3440
+
3441
+ class YamlConfigRenderer(ConfigRenderer[YamlConfigData]):
3442
+ data_cls = YamlConfigData
3443
+
3444
+ def render(self, d: YamlConfigData) -> str:
3445
+ return __import__('yaml').safe_dump(d.obj)
3446
+
3447
+
3448
+ ##
3449
+
3450
+
3451
+ @dc.dataclass(frozen=True)
3452
+ class IniConfigData(ConfigData):
3453
+ sections: IniSectionSettingsMap
3454
+
3455
+ def as_map(self) -> ConfigMap:
3456
+ return self.sections
3457
+
3458
+
3459
+ class IniConfigLoader(ConfigLoader[IniConfigData]):
3460
+ file_exts = ('ini',)
3461
+
3462
+ def load_str(self, s: str) -> IniConfigData:
3463
+ cp = configparser.ConfigParser()
3464
+ cp.read_string(s)
3465
+ return IniConfigData(extract_ini_sections(cp))
3466
+
3467
+
3468
+ class IniConfigRenderer(ConfigRenderer[IniConfigData]):
3469
+ data_cls = IniConfigData
3470
+
3471
+ def render(self, d: IniConfigData) -> str:
3472
+ return render_ini_sections(d.sections)
3473
+
3474
+
3475
+ ##
3476
+
3477
+
3478
+ @dc.dataclass(frozen=True)
3479
+ class SwitchedConfigFileLoader:
3480
+ loaders: ta.Sequence[ConfigLoader]
3481
+ default: ta.Optional[ConfigLoader] = None
3482
+
3483
+ def load_file(self, p: str) -> ConfigData:
3484
+ n = os.path.basename(p)
3485
+
3486
+ for l in self.loaders:
3487
+ if l.match_file(n):
3488
+ return l.load_file(p)
3489
+
3490
+ if (d := self.default) is not None:
3491
+ return d.load_file(p)
3492
+
3493
+ raise NameError(n)
3494
+
3495
+
3496
+ DEFAULT_CONFIG_LOADERS: ta.Sequence[ConfigLoader] = [
3497
+ JsonConfigLoader(),
3498
+ TomlConfigLoader(),
3499
+ YamlConfigLoader(),
3500
+ IniConfigLoader(),
3501
+ ]
3502
+
3503
+ DEFAULT_CONFIG_LOADER: ConfigLoader = JsonConfigLoader()
3504
+
3505
+ DEFAULT_CONFIG_FILE_LOADER = SwitchedConfigFileLoader(
3506
+ loaders=DEFAULT_CONFIG_LOADERS,
3507
+ default=DEFAULT_CONFIG_LOADER,
3508
+ )
3509
+
3510
+
3511
+ ##
3512
+
3513
+
3514
+ @dc.dataclass(frozen=True)
3515
+ class SwitchedConfigRenderer:
3516
+ renderers: ta.Sequence[ConfigRenderer]
3517
+
3518
+ def render(self, d: ConfigData) -> str:
3519
+ for r in self.renderers:
3520
+ if r.match_data(d):
3521
+ return r.render(d)
3522
+ raise TypeError(d)
3523
+
3524
+
3525
+ DEFAULT_CONFIG_RENDERERS: ta.Sequence[ConfigRenderer] = [
3526
+ JsonConfigRenderer(),
3527
+ TomlConfigRenderer(),
3528
+ YamlConfigRenderer(),
3529
+ IniConfigRenderer(),
3530
+ ]
3531
+
3532
+ DEFAULT_CONFIG_RENDERER = SwitchedConfigRenderer(DEFAULT_CONFIG_RENDERERS)
3533
+
3534
+
3535
+ ########################################
3536
+ # ../../../omlish/configs/processing/names.py
3537
+ """
3538
+ usecase: supervisor process groups
3539
+ """
3540
+
3541
+
3542
+ ##
3543
+
3544
+
3545
+ def build_config_named_children(
3546
+ o: ta.Union[
3547
+ ta.Sequence[ConfigMap],
3548
+ ta.Mapping[str, ConfigMap],
3549
+ None,
3550
+ ],
3551
+ *,
3552
+ name_key: str = 'name',
3553
+ ) -> ta.Optional[ta.Sequence[ConfigMap]]:
3554
+ if o is None:
3555
+ return None
3556
+
3557
+ lst: ta.List[ConfigMap] = []
3558
+ if isinstance(o, ta.Mapping):
3559
+ for k, v in o.items():
3560
+ check.isinstance(v, ta.Mapping)
3561
+ if name_key in v:
3562
+ n = v[name_key]
3563
+ if k != n:
3564
+ raise KeyError(f'Given names do not match: {n} != {k}')
3565
+ lst.append(v)
3566
+ else:
3567
+ lst.append({name_key: k, **v})
3568
+
3569
+ else:
3570
+ check.not_isinstance(o, str)
3571
+ lst.extend(o)
3572
+
3573
+ seen = set()
3574
+ for d in lst:
3575
+ n = d['name']
3576
+ if n in d:
3577
+ raise KeyError(f'Duplicate name: {n}')
3578
+ seen.add(n)
3579
+
3580
+ return lst
3581
+
3582
+
3128
3583
  ########################################
3129
3584
  # ../../../omlish/http/parsing.py
3130
3585
  # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -5858,94 +6313,302 @@ class SocketHandler(abc.ABC):
5858
6313
 
5859
6314
 
5860
6315
  ########################################
5861
- # ../../configs.py
6316
+ # ../configs.py
5862
6317
 
5863
6318
 
5864
6319
  ##
5865
6320
 
5866
6321
 
5867
- def parse_config_file(
5868
- name: str,
5869
- f: ta.TextIO,
5870
- ) -> ConfigMapping:
5871
- if name.endswith('.toml'):
5872
- return toml_loads(f.read())
5873
-
5874
- elif any(name.endswith(e) for e in ('.yml', '.yaml')):
5875
- yaml = __import__('yaml')
5876
- return yaml.safe_load(f)
5877
-
5878
- elif name.endswith('.ini'):
5879
- import configparser
5880
- cp = configparser.ConfigParser()
5881
- cp.read_file(f)
5882
- config_dct: ta.Dict[str, ta.Any] = {}
5883
- for sec in cp.sections():
5884
- cd = config_dct
5885
- for k in sec.split('.'):
5886
- cd = cd.setdefault(k, {})
5887
- cd.update(cp.items(sec))
5888
- return config_dct
5889
-
5890
- else:
5891
- return json.loads(f.read())
5892
-
5893
-
5894
- def read_config_file(
5895
- path: str,
5896
- cls: ta.Type[T],
5897
- *,
5898
- prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
5899
- msh: ObjMarshalerManager = OBJ_MARSHALER_MANAGER,
5900
- ) -> T:
5901
- with open(path) as cf:
5902
- config_dct = parse_config_file(os.path.basename(path), cf)
6322
+ class RestartWhenExitUnexpected:
6323
+ pass
5903
6324
 
5904
- if prepare is not None:
5905
- config_dct = prepare(config_dct)
5906
6325
 
5907
- return msh.unmarshal_obj(config_dct, cls)
6326
+ class RestartUnconditionally:
6327
+ pass
5908
6328
 
5909
6329
 
5910
6330
  ##
5911
6331
 
5912
6332
 
5913
- def build_config_named_children(
5914
- o: ta.Union[
5915
- ta.Sequence[ConfigMapping],
5916
- ta.Mapping[str, ConfigMapping],
5917
- None,
5918
- ],
5919
- *,
5920
- name_key: str = 'name',
5921
- ) -> ta.Optional[ta.Sequence[ConfigMapping]]:
5922
- if o is None:
5923
- return None
6333
+ @dc.dataclass(frozen=True)
6334
+ class ProcessConfig:
6335
+ # A Python string expression that is used to compose the supervisor process name for this process. You usually don't
6336
+ # need to worry about setting this unless you change numprocs. The string expression is evaluated against a
6337
+ # dictionary that includes group_name, host_node_name, process_num, program_name, and here (the directory of the
6338
+ # supervisord config file).
6339
+ name: str
5924
6340
 
5925
- lst: ta.List[ConfigMapping] = []
5926
- if isinstance(o, ta.Mapping):
5927
- for k, v in o.items():
5928
- check.isinstance(v, ta.Mapping)
5929
- if name_key in v:
5930
- n = v[name_key]
5931
- if k != n:
5932
- raise KeyError(f'Given names do not match: {n} != {k}')
5933
- lst.append(v)
5934
- else:
5935
- lst.append({name_key: k, **v})
6341
+ # The command that will be run when this program is started. The command can be either absolute (e.g.
6342
+ # /path/to/programname) or relative (e.g. programname). If it is relative, the supervisord's environment $PATH will
6343
+ # be searched for the executable. Programs can accept arguments, e.g. /path/to/program foo bar. The command line can
6344
+ # use double quotes to group arguments with spaces in them to pass to the program, e.g. /path/to/program/name -p
6345
+ # "foo bar". Note that the value of command may include Python string expressions, e.g. /path/to/programname
6346
+ # --port=80%(process_num)02d might expand to /path/to/programname --port=8000 at runtime. String expressions are
6347
+ # evaluated against a dictionary containing the keys group_name, host_node_name, program_name, process_num,
6348
+ # numprocs, here (the directory of the supervisord config file), and all supervisord's environment variables
6349
+ # prefixed with ENV_. Controlled programs should themselves not be daemons, as supervisord assumes it is responsible
6350
+ # for daemonizing its subprocesses
6351
+ command: str
5936
6352
 
5937
- else:
5938
- check.not_isinstance(o, str)
5939
- lst.extend(o)
6353
+ #
5940
6354
 
5941
- seen = set()
5942
- for d in lst:
5943
- n = d['name']
5944
- if n in d:
5945
- raise KeyError(f'Duplicate name: {n}')
5946
- seen.add(n)
6355
+ # Supervisor will start as many instances of this program as named by numprocs. Note that if numprocs > 1, the
6356
+ # process_name expression must include %(process_num)s (or any other valid Python string expression that includes
6357
+ # process_num) within it.
6358
+ num_procs: int = 1
5947
6359
 
5948
- return lst
6360
+ # An integer offset that is used to compute the number at which process_num starts.
6361
+ num_procs_start: int = 0
6362
+
6363
+ #
6364
+
6365
+ # Instruct supervisord to use this UNIX user account as the account which runs the program. The user can only be
6366
+ # switched if supervisord is run as the root user. If supervisord can't switch to the specified user, the program
6367
+ # will not be started.
6368
+ #
6369
+ # Note: The user will be changed using setuid only. This does not start a login shell and does not change
6370
+ # environment variables like USER or HOME
6371
+ user: ta.Optional[str] = None
6372
+ uid: ta.Optional[int] = None
6373
+
6374
+ # An octal number (e.g. 002, 022) representing the umask of the process.
6375
+ umask: ta.Optional[int] = None
6376
+
6377
+ #
6378
+
6379
+ # A file path representing a directory to which supervisord should temporarily chdir before exec'ing the child.
6380
+ directory: ta.Optional[str] = None
6381
+
6382
+ # A list of key/value pairs in the form KEY="val",KEY2="val2" that will be placed in the child process' environment.
6383
+ # The environment string may contain Python string expressions that will be evaluated against a dictionary
6384
+ # containing group_name, host_node_name, process_num, program_name, and here (the directory of the supervisord
6385
+ # config file). Values containing non-alphanumeric characters should be quoted (e.g. KEY="val:123",KEY2="val,456").
6386
+ # Otherwise, quoting the values is optional but recommended. Note that the subprocess will inherit the environment
6387
+ # variables of the shell used to start “supervisord” except for the ones overridden here.
6388
+ environment: ta.Optional[ta.Mapping[str, str]] = None
6389
+
6390
+ #
6391
+
6392
+ # The relative priority of the program in the start and shutdown ordering. Lower priorities indicate programs that
6393
+ # start first and shut down last at startup and when aggregate commands are used in various clients (e.g. “start
6394
+ # all”/”stop all”). Higher priorities indicate programs that start last and shut down first.
6395
+ priority: int = 999
6396
+
6397
+ # If true, this program will start automatically when supervisord is started.
6398
+ auto_start: bool = True
6399
+
6400
+ # Specifies if supervisord should automatically restart a process if it exits when it is in the RUNNING state. May
6401
+ # be one of false, unexpected, or true. If false, the process will not be autorestarted. If unexpected, the process
6402
+ # will be restarted when the program exits with an exit code that is not one of the exit codes associated with this
6403
+ # process' configuration (see exitcodes). If true, the process will be unconditionally restarted when it exits,
6404
+ # without regard to its exit code.
6405
+ #
6406
+ # Note: autorestart controls whether supervisord will autorestart a program if it exits after it has successfully
6407
+ # started up (the process is in the RUNNING state). supervisord has a different restart mechanism for when the
6408
+ # process is starting up (the process is in the STARTING state). Retries during process startup are controlled by
6409
+ # startsecs and startretries.
6410
+ auto_restart: str = 'unexpected'
6411
+
6412
+ # The total number of seconds which the program needs to stay running after a startup to consider the start
6413
+ # successful (moving the process from the STARTING state to the RUNNING state). Set to 0 to indicate that the
6414
+ # program needn't stay running for any particular amount of time.
6415
+ #
6416
+ # Note: Even if a process exits with an “expected” exit code (see exitcodes), the start will still be considered a
6417
+ # failure if the process exits quicker than startsecs.
6418
+ start_secs: int = 1
6419
+
6420
+ # The number of serial failure attempts that supervisord will allow when attempting to start the program before
6421
+ # giving up and putting the process into an FATAL state.
6422
+ #
6423
+ # Note: After each failed restart, process will be put in BACKOFF state and each retry attempt will take
6424
+ # increasingly more time.
6425
+ start_retries: int = 3
6426
+
6427
+ # The signal used to kill the program when a stop is requested. This can be specified using the signal's name or its
6428
+ # number. It is normally one of: TERM, HUP, INT, QUIT, KILL, USR1, or USR2.
6429
+ stop_signal: int = signal.SIGTERM
6430
+
6431
+ # The number of seconds to wait for the OS to return a SIGCHLD to supervisord after the program has been sent a
6432
+ # stopsignal. If this number of seconds elapses before supervisord receives a SIGCHLD from the process, supervisord
6433
+ # will attempt to kill it with a final SIGKILL.
6434
+ stop_wait_secs: int = 10
6435
+
6436
+ # If true, the flag causes supervisor to send the stop signal to the whole process group and implies killasgroup is
6437
+ # true. This is useful for programs, such as Flask in debug mode, that do not propagate stop signals to their
6438
+ # children, leaving them orphaned.
6439
+ stop_as_group: bool = False
6440
+
6441
+ # If true, when resorting to send SIGKILL to the program to terminate it send it to its whole process group instead,
6442
+ # taking care of its children as well, useful e.g with Python programs using multiprocessing.
6443
+ kill_as_group: bool = False
6444
+
6445
+ # The list of “expected” exit codes for this program used with autorestart. If the autorestart parameter is set to
6446
+ # unexpected, and the process exits in any other way than as a result of a supervisor stop request, supervisord will
6447
+ # restart the process if it exits with an exit code that is not defined in this list.
6448
+ #
6449
+ # Note: In Supervisor versions prior to 4.0, the default was 0,2. In Supervisor 4.0, the default was changed to 0.
6450
+ exitcodes: ta.Sequence[int] = (0,)
6451
+
6452
+ #
6453
+
6454
+ @dc.dataclass(frozen=True)
6455
+ class Log:
6456
+ file: ta.Optional[str] = None
6457
+ capture_max_bytes: ta.Optional[int] = None
6458
+ events_enabled: bool = False
6459
+ syslog: bool = False
6460
+ backups: ta.Optional[int] = None
6461
+ max_bytes: ta.Optional[int] = None
6462
+
6463
+ stdout: Log = Log()
6464
+ stderr: Log = Log()
6465
+
6466
+ # If true, cause the process' stderr output to be sent back to supervisord on its stdout file descriptor (in UNIX
6467
+ # shell terms, this is the equivalent of executing /the/program 2>&1).
6468
+ #
6469
+ # Note: Do not set redirect_stderr=true in an [eventlistener:x] section. Eventlisteners use stdout and stdin to
6470
+ # communicate with supervisord. If stderr is redirected, output from stderr will interfere with the eventlistener
6471
+ # protocol.
6472
+ redirect_stderr: bool = False
6473
+
6474
+
6475
+ @dc.dataclass(frozen=True)
6476
+ class ProcessGroupConfig:
6477
+ name: str
6478
+
6479
+ priority: int = 999
6480
+
6481
+ processes: ta.Optional[ta.Sequence[ProcessConfig]] = None
6482
+
6483
+
6484
+ @dc.dataclass(frozen=True)
6485
+ class ServerConfig:
6486
+ # Instruct supervisord to switch users to this UNIX user account before doing any meaningful processing. The user
6487
+ # can only be switched if supervisord is started as the root user.
6488
+ user: ta.Optional[str] = None
6489
+
6490
+ # If true, supervisord will start in the foreground instead of daemonizing.
6491
+ nodaemon: bool = False
6492
+
6493
+ # The umask of the supervisord process.
6494
+ umask: int = 0o22
6495
+
6496
+ #
6497
+
6498
+ # When supervisord daemonizes, switch to this directory. This option can include the value %(here)s, which expands
6499
+ # to the directory in which the supervisord configuration file was found.
6500
+ directory: ta.Optional[str] = None
6501
+
6502
+ # The location in which supervisord keeps its pid file. This option can include the value %(here)s, which expands to
6503
+ # the directory in which the supervisord configuration file was found.
6504
+ pidfile: str = 'supervisord.pid'
6505
+
6506
+ # The identifier string for this supervisor process, used by the RPC interface.
6507
+ identifier: str = 'supervisor'
6508
+
6509
+ # The minimum number of file descriptors that must be available before supervisord will start successfully.
6510
+ min_fds: int = 1024
6511
+ # The minimum number of process descriptors that must be available before supervisord will start successfully.
6512
+ min_procs: int = 200
6513
+
6514
+ # Prevent supervisord from clearing any existing AUTO child log files at startup time. Useful for debugging
6515
+ nocleanup: bool = False
6516
+
6517
+ # Strip all ANSI escape sequences from child log files.
6518
+ strip_ansi: bool = False
6519
+
6520
+ #
6521
+
6522
+ # The path to the activity log of the supervisord process. This option can include the value %(here)s, which expands
6523
+ # to the directory in which the supervisord configuration file was found.
6524
+ logfile: str = 'supervisord.log'
6525
+
6526
+ # The maximum number of bytes that may be consumed by the activity log file before it is rotated (suffix multipliers
6527
+ # like “KB”, “MB”, and “GB” can be used in the value). Set this value to 0 to indicate an unlimited log size.
6528
+ logfile_max_bytes: int = 50 * 1024 * 1024
6529
+
6530
+ # The number of backups to keep around resulting from activity log file rotation. If set to 0, no backups will be
6531
+ # kept.
6532
+ logfile_backups: int = 10
6533
+
6534
+ # The logging level, dictating what is written to the supervisord activity log. One of critical, error, warn, info,
6535
+ # debug, trace, or blather. Note that at log level debug, the supervisord log file will record the stderr/stdout
6536
+ # output of its child processes and extended info about process state changes, which is useful for debugging a
6537
+ # process which isn't starting properly.
6538
+ loglevel: int = logging.INFO
6539
+
6540
+ # The directory used for AUTO child log files. This option can include the value %(here)s, which expands to the
6541
+ # directory in which the supervisord configuration file was found.
6542
+ child_logdir: str = '/dev/null'
6543
+
6544
+ # If true and not daemonized, logs will not be directed to stdout.
6545
+ silent: bool = False
6546
+
6547
+ #
6548
+
6549
+ groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
6550
+
6551
+ # TODO: implement - make sure to accept broken symlinks
6552
+ group_config_dirs: ta.Optional[ta.Sequence[str]] = None
6553
+
6554
+ #
6555
+
6556
+ http_port: ta.Optional[int] = None
6557
+
6558
+ #
6559
+
6560
+ @classmethod
6561
+ def new(
6562
+ cls,
6563
+ *,
6564
+ umask: ta.Union[int, str] = 0o22,
6565
+ directory: ta.Optional[str] = None,
6566
+ logfile: str = 'supervisord.log',
6567
+ logfile_max_bytes: ta.Union[int, str] = 50 * 1024 * 1024,
6568
+ loglevel: ta.Union[int, str] = logging.INFO,
6569
+ pidfile: str = 'supervisord.pid',
6570
+ child_logdir: ta.Optional[str] = None,
6571
+ **kwargs: ta.Any,
6572
+ ) -> 'ServerConfig':
6573
+ return cls(
6574
+ umask=parse_octal(umask),
6575
+ directory=check_existing_dir(directory) if directory is not None else None,
6576
+ logfile=check_path_with_existing_dir(logfile),
6577
+ logfile_max_bytes=parse_bytes_size(logfile_max_bytes),
6578
+ loglevel=parse_logging_level(loglevel),
6579
+ pidfile=check_path_with_existing_dir(pidfile),
6580
+ child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
6581
+ **kwargs,
6582
+ )
6583
+
6584
+
6585
+ ##
6586
+
6587
+
6588
+ def prepare_process_group_config(dct: ConfigMap) -> ConfigMap:
6589
+ out = dict(dct)
6590
+ out['processes'] = build_config_named_children(out.get('processes'))
6591
+ return out
6592
+
6593
+
6594
+ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
6595
+ out = dict(dct)
6596
+ group_dcts = build_config_named_children(out.get('groups'))
6597
+ out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
6598
+ return out
6599
+
6600
+
6601
+ ##
6602
+
6603
+
6604
+ def parse_logging_level(value: ta.Union[str, int]) -> int:
6605
+ if isinstance(value, int):
6606
+ return value
6607
+ s = str(value).lower()
6608
+ level = logging.getLevelNamesMapping().get(s.upper())
6609
+ if level is None:
6610
+ raise ValueError(f'bad logging level name {value!r}')
6611
+ return level
5949
6612
 
5950
6613
 
5951
6614
  ########################################
@@ -6095,6 +6758,38 @@ class UnsupportedMethodHttpHandlerError(Exception):
6095
6758
  pass
6096
6759
 
6097
6760
 
6761
+ ########################################
6762
+ # ../../../omlish/lite/configs.py
6763
+
6764
+
6765
+ ##
6766
+
6767
+
6768
+ def load_config_file_obj(
6769
+ f: str,
6770
+ cls: ta.Type[T],
6771
+ *,
6772
+ prepare: ta.Union[
6773
+ ta.Callable[[ConfigMap], ConfigMap],
6774
+ ta.Iterable[ta.Callable[[ConfigMap], ConfigMap]],
6775
+ ] = (),
6776
+ msh: ObjMarshalerManager = OBJ_MARSHALER_MANAGER,
6777
+ ) -> T:
6778
+ config_data = DEFAULT_CONFIG_FILE_LOADER.load_file(f)
6779
+
6780
+ config_dct = config_data.as_map()
6781
+
6782
+ if prepare is not None:
6783
+ if isinstance(prepare, ta.Iterable):
6784
+ pfs = list(prepare)
6785
+ else:
6786
+ pfs = [prepare]
6787
+ for pf in pfs:
6788
+ config_dct = pf(config_dct)
6789
+
6790
+ return msh.unmarshal_obj(config_dct, cls)
6791
+
6792
+
6098
6793
  ########################################
6099
6794
  # ../../../omlish/logs/standard.py
6100
6795
  """
@@ -6217,303 +6912,184 @@ def configure_standard_logging(
6217
6912
  return StandardConfiguredLogHandler(handler)
6218
6913
 
6219
6914
 
6220
- ########################################
6221
- # ../configs.py
6222
-
6223
-
6224
- ##
6225
-
6226
-
6227
- class RestartWhenExitUnexpected:
6228
- pass
6229
-
6230
-
6231
- class RestartUnconditionally:
6232
- pass
6233
-
6234
-
6235
- ##
6236
-
6237
-
6238
- @dc.dataclass(frozen=True)
6239
- class ProcessConfig:
6240
- # A Python string expression that is used to compose the supervisor process name for this process. You usually don't
6241
- # need to worry about setting this unless you change numprocs. The string expression is evaluated against a
6242
- # dictionary that includes group_name, host_node_name, process_num, program_name, and here (the directory of the
6243
- # supervisord config file).
6244
- name: str
6245
-
6246
- # The command that will be run when this program is started. The command can be either absolute (e.g.
6247
- # /path/to/programname) or relative (e.g. programname). If it is relative, the supervisord's environment $PATH will
6248
- # be searched for the executable. Programs can accept arguments, e.g. /path/to/program foo bar. The command line can
6249
- # use double quotes to group arguments with spaces in them to pass to the program, e.g. /path/to/program/name -p
6250
- # "foo bar". Note that the value of command may include Python string expressions, e.g. /path/to/programname
6251
- # --port=80%(process_num)02d might expand to /path/to/programname --port=8000 at runtime. String expressions are
6252
- # evaluated against a dictionary containing the keys group_name, host_node_name, program_name, process_num,
6253
- # numprocs, here (the directory of the supervisord config file), and all supervisord's environment variables
6254
- # prefixed with ENV_. Controlled programs should themselves not be daemons, as supervisord assumes it is responsible
6255
- # for daemonizing its subprocesses
6256
- command: str
6257
-
6258
- #
6259
-
6260
- # Supervisor will start as many instances of this program as named by numprocs. Note that if numprocs > 1, the
6261
- # process_name expression must include %(process_num)s (or any other valid Python string expression that includes
6262
- # process_num) within it.
6263
- num_procs: int = 1
6264
-
6265
- # An integer offset that is used to compute the number at which process_num starts.
6266
- num_procs_start: int = 0
6267
-
6268
- #
6269
-
6270
- # Instruct supervisord to use this UNIX user account as the account which runs the program. The user can only be
6271
- # switched if supervisord is run as the root user. If supervisord can't switch to the specified user, the program
6272
- # will not be started.
6273
- #
6274
- # Note: The user will be changed using setuid only. This does not start a login shell and does not change
6275
- # environment variables like USER or HOME
6276
- user: ta.Optional[str] = None
6277
- uid: ta.Optional[int] = None
6278
-
6279
- # An octal number (e.g. 002, 022) representing the umask of the process.
6280
- umask: ta.Optional[int] = None
6281
-
6282
- #
6283
-
6284
- # A file path representing a directory to which supervisord should temporarily chdir before exec'ing the child.
6285
- directory: ta.Optional[str] = None
6286
-
6287
- # A list of key/value pairs in the form KEY="val",KEY2="val2" that will be placed in the child process' environment.
6288
- # The environment string may contain Python string expressions that will be evaluated against a dictionary
6289
- # containing group_name, host_node_name, process_num, program_name, and here (the directory of the supervisord
6290
- # config file). Values containing non-alphanumeric characters should be quoted (e.g. KEY="val:123",KEY2="val,456").
6291
- # Otherwise, quoting the values is optional but recommended. Note that the subprocess will inherit the environment
6292
- # variables of the shell used to start “supervisord” except for the ones overridden here.
6293
- environment: ta.Optional[ta.Mapping[str, str]] = None
6294
-
6295
- #
6296
-
6297
- # The relative priority of the program in the start and shutdown ordering. Lower priorities indicate programs that
6298
- # start first and shut down last at startup and when aggregate commands are used in various clients (e.g. “start
6299
- # all”/”stop all”). Higher priorities indicate programs that start last and shut down first.
6300
- priority: int = 999
6301
-
6302
- # If true, this program will start automatically when supervisord is started.
6303
- auto_start: bool = True
6304
-
6305
- # Specifies if supervisord should automatically restart a process if it exits when it is in the RUNNING state. May
6306
- # be one of false, unexpected, or true. If false, the process will not be autorestarted. If unexpected, the process
6307
- # will be restarted when the program exits with an exit code that is not one of the exit codes associated with this
6308
- # process' configuration (see exitcodes). If true, the process will be unconditionally restarted when it exits,
6309
- # without regard to its exit code.
6310
- #
6311
- # Note: autorestart controls whether supervisord will autorestart a program if it exits after it has successfully
6312
- # started up (the process is in the RUNNING state). supervisord has a different restart mechanism for when the
6313
- # process is starting up (the process is in the STARTING state). Retries during process startup are controlled by
6314
- # startsecs and startretries.
6315
- auto_restart: str = 'unexpected'
6915
+ ########################################
6916
+ # ../types.py
6316
6917
 
6317
- # The total number of seconds which the program needs to stay running after a startup to consider the start
6318
- # successful (moving the process from the STARTING state to the RUNNING state). Set to 0 to indicate that the
6319
- # program needn't stay running for any particular amount of time.
6320
- #
6321
- # Note: Even if a process exits with an “expected” exit code (see exitcodes), the start will still be considered a
6322
- # failure if the process exits quicker than startsecs.
6323
- start_secs: int = 1
6324
6918
 
6325
- # The number of serial failure attempts that supervisord will allow when attempting to start the program before
6326
- # giving up and putting the process into an FATAL state.
6327
- #
6328
- # Note: After each failed restart, process will be put in BACKOFF state and each retry attempt will take
6329
- # increasingly more time.
6330
- start_retries: int = 3
6919
+ ##
6331
6920
 
6332
- # The signal used to kill the program when a stop is requested. This can be specified using the signal's name or its
6333
- # number. It is normally one of: TERM, HUP, INT, QUIT, KILL, USR1, or USR2.
6334
- stop_signal: int = signal.SIGTERM
6335
6921
 
6336
- # The number of seconds to wait for the OS to return a SIGCHLD to supervisord after the program has been sent a
6337
- # stopsignal. If this number of seconds elapses before supervisord receives a SIGCHLD from the process, supervisord
6338
- # will attempt to kill it with a final SIGKILL.
6339
- stop_wait_secs: int = 10
6922
+ class ExitNow(Exception): # noqa
6923
+ pass
6340
6924
 
6341
- # If true, the flag causes supervisor to send the stop signal to the whole process group and implies killasgroup is
6342
- # true. This is useful for programs, such as Flask in debug mode, that do not propagate stop signals to their
6343
- # children, leaving them orphaned.
6344
- stop_as_group: bool = False
6345
6925
 
6346
- # If true, when resorting to send SIGKILL to the program to terminate it send it to its whole process group instead,
6347
- # taking care of its children as well, useful e.g with Python programs using multiprocessing.
6348
- kill_as_group: bool = False
6926
+ ServerEpoch = ta.NewType('ServerEpoch', int)
6349
6927
 
6350
- # The list of “expected” exit codes for this program used with autorestart. If the autorestart parameter is set to
6351
- # unexpected, and the process exits in any other way than as a result of a supervisor stop request, supervisord will
6352
- # restart the process if it exits with an exit code that is not defined in this list.
6353
- #
6354
- # Note: In Supervisor versions prior to 4.0, the default was 0,2. In Supervisor 4.0, the default was changed to 0.
6355
- exitcodes: ta.Sequence[int] = (0,)
6356
6928
 
6357
- #
6929
+ ##
6358
6930
 
6359
- @dc.dataclass(frozen=True)
6360
- class Log:
6361
- file: ta.Optional[str] = None
6362
- capture_max_bytes: ta.Optional[int] = None
6363
- events_enabled: bool = False
6364
- syslog: bool = False
6365
- backups: ta.Optional[int] = None
6366
- max_bytes: ta.Optional[int] = None
6367
6931
 
6368
- stdout: Log = Log()
6369
- stderr: Log = Log()
6932
+ @functools.total_ordering
6933
+ class ConfigPriorityOrdered(abc.ABC):
6934
+ @property
6935
+ @abc.abstractmethod
6936
+ def config(self) -> ta.Any:
6937
+ raise NotImplementedError
6370
6938
 
6371
- # If true, cause the process' stderr output to be sent back to supervisord on its stdout file descriptor (in UNIX
6372
- # shell terms, this is the equivalent of executing /the/program 2>&1).
6373
- #
6374
- # Note: Do not set redirect_stderr=true in an [eventlistener:x] section. Eventlisteners use stdout and stdin to
6375
- # communicate with supervisord. If stderr is redirected, output from stderr will interfere with the eventlistener
6376
- # protocol.
6377
- redirect_stderr: bool = False
6939
+ def __lt__(self, other):
6940
+ return self.config.priority < other.config.priority
6378
6941
 
6942
+ def __eq__(self, other):
6943
+ return self.config.priority == other.config.priority
6379
6944
 
6380
- @dc.dataclass(frozen=True)
6381
- class ProcessGroupConfig:
6382
- name: str
6383
6945
 
6384
- priority: int = 999
6946
+ ##
6385
6947
 
6386
- processes: ta.Optional[ta.Sequence[ProcessConfig]] = None
6387
6948
 
6949
+ class SupervisorStateManager(abc.ABC):
6950
+ @property
6951
+ @abc.abstractmethod
6952
+ def state(self) -> SupervisorState:
6953
+ raise NotImplementedError
6388
6954
 
6389
- @dc.dataclass(frozen=True)
6390
- class ServerConfig:
6391
- # Instruct supervisord to switch users to this UNIX user account before doing any meaningful processing. The user
6392
- # can only be switched if supervisord is started as the root user.
6393
- user: ta.Optional[str] = None
6955
+ @abc.abstractmethod
6956
+ def set_state(self, state: SupervisorState) -> None:
6957
+ raise NotImplementedError
6394
6958
 
6395
- # If true, supervisord will start in the foreground instead of daemonizing.
6396
- nodaemon: bool = False
6397
6959
 
6398
- # The umask of the supervisord process.
6399
- umask: int = 0o22
6960
+ ##
6400
6961
 
6401
- #
6402
6962
 
6403
- # When supervisord daemonizes, switch to this directory. This option can include the value %(here)s, which expands
6404
- # to the directory in which the supervisord configuration file was found.
6405
- directory: ta.Optional[str] = None
6963
+ class HasDispatchers(abc.ABC):
6964
+ @abc.abstractmethod
6965
+ def get_dispatchers(self) -> 'Dispatchers':
6966
+ raise NotImplementedError
6406
6967
 
6407
- # The location in which supervisord keeps its pid file. This option can include the value %(here)s, which expands to
6408
- # the directory in which the supervisord configuration file was found.
6409
- pidfile: str = 'supervisord.pid'
6410
6968
 
6411
- # The identifier string for this supervisor process, used by the RPC interface.
6412
- identifier: str = 'supervisor'
6969
+ class ProcessDispatcher(FdioHandler, abc.ABC):
6970
+ @property
6971
+ @abc.abstractmethod
6972
+ def channel(self) -> ProcessOutputChannel:
6973
+ raise NotImplementedError
6413
6974
 
6414
- # The minimum number of file descriptors that must be available before supervisord will start successfully.
6415
- min_fds: int = 1024
6416
- # The minimum number of process descriptors that must be available before supervisord will start successfully.
6417
- min_procs: int = 200
6975
+ @property
6976
+ @abc.abstractmethod
6977
+ def process(self) -> 'Process':
6978
+ raise NotImplementedError
6418
6979
 
6419
- # Prevent supervisord from clearing any existing AUTO child log files at startup time. Useful for debugging
6420
- nocleanup: bool = False
6421
6980
 
6422
- # Strip all ANSI escape sequences from child log files.
6423
- strip_ansi: bool = False
6981
+ class ProcessOutputDispatcher(ProcessDispatcher, abc.ABC):
6982
+ @abc.abstractmethod
6983
+ def remove_logs(self) -> None:
6984
+ raise NotImplementedError
6424
6985
 
6425
- #
6986
+ @abc.abstractmethod
6987
+ def reopen_logs(self) -> None:
6988
+ raise NotImplementedError
6426
6989
 
6427
- # The path to the activity log of the supervisord process. This option can include the value %(here)s, which expands
6428
- # to the directory in which the supervisord configuration file was found.
6429
- logfile: str = 'supervisord.log'
6430
6990
 
6431
- # The maximum number of bytes that may be consumed by the activity log file before it is rotated (suffix multipliers
6432
- # like “KB”, “MB”, and “GB” can be used in the value). Set this value to 0 to indicate an unlimited log size.
6433
- logfile_max_bytes: int = 50 * 1024 * 1024
6991
+ class ProcessInputDispatcher(ProcessDispatcher, abc.ABC):
6992
+ @abc.abstractmethod
6993
+ def write(self, chars: ta.Union[bytes, str]) -> None:
6994
+ raise NotImplementedError
6434
6995
 
6435
- # The number of backups to keep around resulting from activity log file rotation. If set to 0, no backups will be
6436
- # kept.
6437
- logfile_backups: int = 10
6996
+ @abc.abstractmethod
6997
+ def flush(self) -> None:
6998
+ raise NotImplementedError
6438
6999
 
6439
- # The logging level, dictating what is written to the supervisord activity log. One of critical, error, warn, info,
6440
- # debug, trace, or blather. Note that at log level debug, the supervisord log file will record the stderr/stdout
6441
- # output of its child processes and extended info about process state changes, which is useful for debugging a
6442
- # process which isn't starting properly.
6443
- loglevel: int = logging.INFO
6444
7000
 
6445
- # The directory used for AUTO child log files. This option can include the value %(here)s, which expands to the
6446
- # directory in which the supervisord configuration file was found.
6447
- child_logdir: str = '/dev/null'
7001
+ ##
6448
7002
 
6449
- # If true and not daemonized, logs will not be directed to stdout.
6450
- silent: bool = False
6451
7003
 
6452
- #
7004
+ class Process(
7005
+ ConfigPriorityOrdered,
7006
+ HasDispatchers,
7007
+ abc.ABC,
7008
+ ):
7009
+ @property
7010
+ @abc.abstractmethod
7011
+ def name(self) -> str:
7012
+ raise NotImplementedError
6453
7013
 
6454
- groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
7014
+ @property
7015
+ @abc.abstractmethod
7016
+ def config(self) -> ProcessConfig:
7017
+ raise NotImplementedError
6455
7018
 
6456
- # TODO: implement - make sure to accept broken symlinks
6457
- group_config_dirs: ta.Optional[ta.Sequence[str]] = None
7019
+ @property
7020
+ @abc.abstractmethod
7021
+ def group(self) -> 'ProcessGroup':
7022
+ raise NotImplementedError
7023
+
7024
+ @property
7025
+ @abc.abstractmethod
7026
+ def pid(self) -> Pid:
7027
+ raise NotImplementedError
6458
7028
 
6459
7029
  #
6460
7030
 
6461
- http_port: ta.Optional[int] = None
7031
+ @abc.abstractmethod
7032
+ def finish(self, sts: Rc) -> None:
7033
+ raise NotImplementedError
6462
7034
 
6463
- #
7035
+ @abc.abstractmethod
7036
+ def stop(self) -> ta.Optional[str]:
7037
+ raise NotImplementedError
6464
7038
 
6465
- @classmethod
6466
- def new(
6467
- cls,
6468
- *,
6469
- umask: ta.Union[int, str] = 0o22,
6470
- directory: ta.Optional[str] = None,
6471
- logfile: str = 'supervisord.log',
6472
- logfile_max_bytes: ta.Union[int, str] = 50 * 1024 * 1024,
6473
- loglevel: ta.Union[int, str] = logging.INFO,
6474
- pidfile: str = 'supervisord.pid',
6475
- child_logdir: ta.Optional[str] = None,
6476
- **kwargs: ta.Any,
6477
- ) -> 'ServerConfig':
6478
- return cls(
6479
- umask=parse_octal(umask),
6480
- directory=check_existing_dir(directory) if directory is not None else None,
6481
- logfile=check_path_with_existing_dir(logfile),
6482
- logfile_max_bytes=parse_bytes_size(logfile_max_bytes),
6483
- loglevel=parse_logging_level(loglevel),
6484
- pidfile=check_path_with_existing_dir(pidfile),
6485
- child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
6486
- **kwargs,
6487
- )
7039
+ @abc.abstractmethod
7040
+ def give_up(self) -> None:
7041
+ raise NotImplementedError
7042
+
7043
+ @abc.abstractmethod
7044
+ def transition(self) -> None:
7045
+ raise NotImplementedError
7046
+
7047
+ @property
7048
+ @abc.abstractmethod
7049
+ def state(self) -> ProcessState:
7050
+ raise NotImplementedError
7051
+
7052
+ @abc.abstractmethod
7053
+ def after_setuid(self) -> None:
7054
+ raise NotImplementedError
6488
7055
 
6489
7056
 
6490
7057
  ##
6491
7058
 
6492
7059
 
6493
- def prepare_process_group_config(dct: ConfigMapping) -> ConfigMapping:
6494
- out = dict(dct)
6495
- out['processes'] = build_config_named_children(out.get('processes'))
6496
- return out
7060
+ class ProcessGroup(
7061
+ ConfigPriorityOrdered,
7062
+ KeyedCollectionAccessors[str, Process],
7063
+ abc.ABC,
7064
+ ):
7065
+ @property
7066
+ @abc.abstractmethod
7067
+ def name(self) -> str:
7068
+ raise NotImplementedError
6497
7069
 
7070
+ @property
7071
+ @abc.abstractmethod
7072
+ def config(self) -> ProcessGroupConfig:
7073
+ raise NotImplementedError
6498
7074
 
6499
- def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
6500
- out = dict(dct)
6501
- group_dcts = build_config_named_children(out.get('groups'))
6502
- out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
6503
- return out
7075
+ @property
7076
+ @abc.abstractmethod
7077
+ def by_name(self) -> ta.Mapping[str, Process]:
7078
+ raise NotImplementedError
6504
7079
 
7080
+ #
6505
7081
 
6506
- ##
7082
+ @abc.abstractmethod
7083
+ def stop_all(self) -> None:
7084
+ raise NotImplementedError
6507
7085
 
7086
+ @abc.abstractmethod
7087
+ def get_unstopped_processes(self) -> ta.List[Process]:
7088
+ raise NotImplementedError
6508
7089
 
6509
- def parse_logging_level(value: ta.Union[str, int]) -> int:
6510
- if isinstance(value, int):
6511
- return value
6512
- s = str(value).lower()
6513
- level = logging.getLevelNamesMapping().get(s.upper())
6514
- if level is None:
6515
- raise ValueError(f'bad logging level name {value!r}')
6516
- return level
7090
+ @abc.abstractmethod
7091
+ def before_remove(self) -> None:
7092
+ raise NotImplementedError
6517
7093
 
6518
7094
 
6519
7095
  ########################################
@@ -7022,368 +7598,59 @@ class CoroHttpServer:
7022
7598
  ##
7023
7599
 
7024
7600
 
7025
- class CoroHttpServerSocketHandler(SocketHandler):
7026
- def __init__(
7027
- self,
7028
- client_address: SocketAddress,
7029
- rfile: ta.BinaryIO,
7030
- wfile: ta.BinaryIO,
7031
- *,
7032
- server_factory: CoroHttpServerFactory,
7033
- log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
7034
- ) -> None:
7035
- super().__init__(
7036
- client_address,
7037
- rfile,
7038
- wfile,
7039
- )
7040
-
7041
- self._server_factory = server_factory
7042
- self._log_handler = log_handler
7043
-
7044
- def handle(self) -> None:
7045
- server = self._server_factory(self._client_address)
7046
-
7047
- gen = server.coro_handle()
7048
-
7049
- o = next(gen)
7050
- while True:
7051
- if isinstance(o, CoroHttpServer.AnyLogIo):
7052
- i = None
7053
- if self._log_handler is not None:
7054
- self._log_handler(server, o)
7055
-
7056
- elif isinstance(o, CoroHttpServer.ReadIo):
7057
- i = self._rfile.read(o.sz)
7058
-
7059
- elif isinstance(o, CoroHttpServer.ReadLineIo):
7060
- i = self._rfile.readline(o.sz)
7061
-
7062
- elif isinstance(o, CoroHttpServer.WriteIo):
7063
- i = None
7064
- self._wfile.write(o.data)
7065
- self._wfile.flush()
7066
-
7067
- else:
7068
- raise TypeError(o)
7069
-
7070
- try:
7071
- if i is not None:
7072
- o = gen.send(i)
7073
- else:
7074
- o = next(gen)
7075
- except StopIteration:
7076
- break
7077
-
7078
-
7079
- ########################################
7080
- # ../types.py
7081
-
7082
-
7083
- ##
7084
-
7085
-
7086
- class ExitNow(Exception): # noqa
7087
- pass
7088
-
7089
-
7090
- ServerEpoch = ta.NewType('ServerEpoch', int)
7091
-
7092
-
7093
- ##
7094
-
7095
-
7096
- @functools.total_ordering
7097
- class ConfigPriorityOrdered(abc.ABC):
7098
- @property
7099
- @abc.abstractmethod
7100
- def config(self) -> ta.Any:
7101
- raise NotImplementedError
7102
-
7103
- def __lt__(self, other):
7104
- return self.config.priority < other.config.priority
7105
-
7106
- def __eq__(self, other):
7107
- return self.config.priority == other.config.priority
7108
-
7109
-
7110
- ##
7111
-
7112
-
7113
- class SupervisorStateManager(abc.ABC):
7114
- @property
7115
- @abc.abstractmethod
7116
- def state(self) -> SupervisorState:
7117
- raise NotImplementedError
7118
-
7119
- @abc.abstractmethod
7120
- def set_state(self, state: SupervisorState) -> None:
7121
- raise NotImplementedError
7122
-
7123
-
7124
- ##
7125
-
7126
-
7127
- class HasDispatchers(abc.ABC):
7128
- @abc.abstractmethod
7129
- def get_dispatchers(self) -> 'Dispatchers':
7130
- raise NotImplementedError
7131
-
7132
-
7133
- class ProcessDispatcher(FdioHandler, abc.ABC):
7134
- @property
7135
- @abc.abstractmethod
7136
- def channel(self) -> ProcessOutputChannel:
7137
- raise NotImplementedError
7138
-
7139
- @property
7140
- @abc.abstractmethod
7141
- def process(self) -> 'Process':
7142
- raise NotImplementedError
7143
-
7144
-
7145
- class ProcessOutputDispatcher(ProcessDispatcher, abc.ABC):
7146
- @abc.abstractmethod
7147
- def remove_logs(self) -> None:
7148
- raise NotImplementedError
7149
-
7150
- @abc.abstractmethod
7151
- def reopen_logs(self) -> None:
7152
- raise NotImplementedError
7153
-
7154
-
7155
- class ProcessInputDispatcher(ProcessDispatcher, abc.ABC):
7156
- @abc.abstractmethod
7157
- def write(self, chars: ta.Union[bytes, str]) -> None:
7158
- raise NotImplementedError
7159
-
7160
- @abc.abstractmethod
7161
- def flush(self) -> None:
7162
- raise NotImplementedError
7163
-
7164
-
7165
- ##
7166
-
7167
-
7168
- class Process(
7169
- ConfigPriorityOrdered,
7170
- HasDispatchers,
7171
- abc.ABC,
7172
- ):
7173
- @property
7174
- @abc.abstractmethod
7175
- def name(self) -> str:
7176
- raise NotImplementedError
7177
-
7178
- @property
7179
- @abc.abstractmethod
7180
- def config(self) -> ProcessConfig:
7181
- raise NotImplementedError
7182
-
7183
- @property
7184
- @abc.abstractmethod
7185
- def group(self) -> 'ProcessGroup':
7186
- raise NotImplementedError
7187
-
7188
- @property
7189
- @abc.abstractmethod
7190
- def pid(self) -> Pid:
7191
- raise NotImplementedError
7192
-
7193
- #
7194
-
7195
- @abc.abstractmethod
7196
- def finish(self, sts: Rc) -> None:
7197
- raise NotImplementedError
7198
-
7199
- @abc.abstractmethod
7200
- def stop(self) -> ta.Optional[str]:
7201
- raise NotImplementedError
7202
-
7203
- @abc.abstractmethod
7204
- def give_up(self) -> None:
7205
- raise NotImplementedError
7206
-
7207
- @abc.abstractmethod
7208
- def transition(self) -> None:
7209
- raise NotImplementedError
7210
-
7211
- @property
7212
- @abc.abstractmethod
7213
- def state(self) -> ProcessState:
7214
- raise NotImplementedError
7215
-
7216
- @abc.abstractmethod
7217
- def after_setuid(self) -> None:
7218
- raise NotImplementedError
7219
-
7220
-
7221
- ##
7222
-
7223
-
7224
- class ProcessGroup(
7225
- ConfigPriorityOrdered,
7226
- KeyedCollectionAccessors[str, Process],
7227
- abc.ABC,
7228
- ):
7229
- @property
7230
- @abc.abstractmethod
7231
- def name(self) -> str:
7232
- raise NotImplementedError
7233
-
7234
- @property
7235
- @abc.abstractmethod
7236
- def config(self) -> ProcessGroupConfig:
7237
- raise NotImplementedError
7238
-
7239
- @property
7240
- @abc.abstractmethod
7241
- def by_name(self) -> ta.Mapping[str, Process]:
7242
- raise NotImplementedError
7243
-
7244
- #
7245
-
7246
- @abc.abstractmethod
7247
- def stop_all(self) -> None:
7248
- raise NotImplementedError
7249
-
7250
- @abc.abstractmethod
7251
- def get_unstopped_processes(self) -> ta.List[Process]:
7252
- raise NotImplementedError
7253
-
7254
- @abc.abstractmethod
7255
- def before_remove(self) -> None:
7256
- raise NotImplementedError
7257
-
7258
-
7259
- ########################################
7260
- # ../../../omlish/http/coro/fdio.py
7261
-
7262
-
7263
- class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
7601
+ class CoroHttpServerSocketHandler(SocketHandler):
7264
7602
  def __init__(
7265
7603
  self,
7266
- addr: SocketAddress,
7267
- sock: socket.socket,
7268
- handler: HttpHandler,
7604
+ client_address: SocketAddress,
7605
+ rfile: ta.BinaryIO,
7606
+ wfile: ta.BinaryIO,
7269
7607
  *,
7270
- read_size: int = 0x10000,
7271
- write_size: int = 0x10000,
7608
+ server_factory: CoroHttpServerFactory,
7272
7609
  log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
7273
7610
  ) -> None:
7274
- check.state(not sock.getblocking())
7275
-
7276
- super().__init__(addr, sock)
7277
-
7278
- self._handler = handler
7279
- self._read_size = read_size
7280
- self._write_size = write_size
7281
- self._log_handler = log_handler
7282
-
7283
- self._read_buf = ReadableListBuffer()
7284
- self._write_buf: IncrementalWriteBuffer | None = None
7285
-
7286
- self._coro_srv = CoroHttpServer(
7287
- addr,
7288
- handler=self._handler,
7611
+ super().__init__(
7612
+ client_address,
7613
+ rfile,
7614
+ wfile,
7289
7615
  )
7290
- self._srv_coro: ta.Optional[ta.Generator[CoroHttpServer.Io, ta.Optional[bytes], None]] = self._coro_srv.coro_handle() # noqa
7291
7616
 
7292
- self._cur_io: CoroHttpServer.Io | None = None
7293
- self._next_io()
7617
+ self._server_factory = server_factory
7618
+ self._log_handler = log_handler
7294
7619
 
7295
- #
7620
+ def handle(self) -> None:
7621
+ server = self._server_factory(self._client_address)
7296
7622
 
7297
- def _next_io(self) -> None: # noqa
7298
- coro = check.not_none(self._srv_coro)
7623
+ gen = server.coro_handle()
7299
7624
 
7300
- d: bytes | None = None
7301
- o = self._cur_io
7625
+ o = next(gen)
7302
7626
  while True:
7303
- if o is None:
7304
- try:
7305
- if d is not None:
7306
- o = coro.send(d)
7307
- d = None
7308
- else:
7309
- o = next(coro)
7310
- except StopIteration:
7311
- self.close()
7312
- o = None
7313
- break
7314
-
7315
7627
  if isinstance(o, CoroHttpServer.AnyLogIo):
7628
+ i = None
7316
7629
  if self._log_handler is not None:
7317
- self._log_handler(self._coro_srv, o)
7318
- o = None
7630
+ self._log_handler(server, o)
7319
7631
 
7320
7632
  elif isinstance(o, CoroHttpServer.ReadIo):
7321
- if (d := self._read_buf.read(o.sz)) is None:
7322
- break
7323
- o = None
7633
+ i = self._rfile.read(o.sz)
7324
7634
 
7325
7635
  elif isinstance(o, CoroHttpServer.ReadLineIo):
7326
- if (d := self._read_buf.read_until(b'\n')) is None:
7327
- break
7328
- o = None
7636
+ i = self._rfile.readline(o.sz)
7329
7637
 
7330
7638
  elif isinstance(o, CoroHttpServer.WriteIo):
7331
- check.none(self._write_buf)
7332
- self._write_buf = IncrementalWriteBuffer(o.data, write_size=self._write_size)
7333
- break
7639
+ i = None
7640
+ self._wfile.write(o.data)
7641
+ self._wfile.flush()
7334
7642
 
7335
7643
  else:
7336
7644
  raise TypeError(o)
7337
7645
 
7338
- self._cur_io = o
7339
-
7340
- #
7341
-
7342
- def readable(self) -> bool:
7343
- return True
7344
-
7345
- def writable(self) -> bool:
7346
- return self._write_buf is not None
7347
-
7348
- #
7349
-
7350
- def on_readable(self) -> None:
7351
- try:
7352
- buf = check.not_none(self._sock).recv(self._read_size)
7353
- except BlockingIOError:
7354
- return
7355
- except ConnectionResetError:
7356
- self.close()
7357
- return
7358
- if not buf:
7359
- self.close()
7360
- return
7361
-
7362
- self._read_buf.feed(buf)
7363
-
7364
- if isinstance(self._cur_io, CoroHttpServer.AnyReadIo):
7365
- self._next_io()
7366
-
7367
- def on_writable(self) -> None:
7368
- check.isinstance(self._cur_io, CoroHttpServer.WriteIo)
7369
- wb = check.not_none(self._write_buf)
7370
- while wb.rem > 0:
7371
- def send(d: bytes) -> int:
7372
- try:
7373
- return check.not_none(self._sock).send(d)
7374
- except ConnectionResetError:
7375
- self.close()
7376
- return 0
7377
- except BlockingIOError:
7378
- return 0
7379
- if not wb.write(send):
7646
+ try:
7647
+ if i is not None:
7648
+ o = gen.send(i)
7649
+ else:
7650
+ o = next(gen)
7651
+ except StopIteration:
7380
7652
  break
7381
7653
 
7382
- if wb.rem < 1:
7383
- self._write_buf = None
7384
- self._cur_io = None
7385
- self._next_io()
7386
-
7387
7654
 
7388
7655
  ########################################
7389
7656
  # ../dispatchers.py
@@ -8085,6 +8352,135 @@ class SupervisorSetupImpl(SupervisorSetup):
8085
8352
  os.umask(self._config.umask)
8086
8353
 
8087
8354
 
8355
+ ########################################
8356
+ # ../../../omlish/http/coro/fdio.py
8357
+
8358
+
8359
+ class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
8360
+ def __init__(
8361
+ self,
8362
+ addr: SocketAddress,
8363
+ sock: socket.socket,
8364
+ handler: HttpHandler,
8365
+ *,
8366
+ read_size: int = 0x10000,
8367
+ write_size: int = 0x10000,
8368
+ log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
8369
+ ) -> None:
8370
+ check.state(not sock.getblocking())
8371
+
8372
+ super().__init__(addr, sock)
8373
+
8374
+ self._handler = handler
8375
+ self._read_size = read_size
8376
+ self._write_size = write_size
8377
+ self._log_handler = log_handler
8378
+
8379
+ self._read_buf = ReadableListBuffer()
8380
+ self._write_buf: IncrementalWriteBuffer | None = None
8381
+
8382
+ self._coro_srv = CoroHttpServer(
8383
+ addr,
8384
+ handler=self._handler,
8385
+ )
8386
+ self._srv_coro: ta.Optional[ta.Generator[CoroHttpServer.Io, ta.Optional[bytes], None]] = self._coro_srv.coro_handle() # noqa
8387
+
8388
+ self._cur_io: CoroHttpServer.Io | None = None
8389
+ self._next_io()
8390
+
8391
+ #
8392
+
8393
+ def _next_io(self) -> None: # noqa
8394
+ coro = check.not_none(self._srv_coro)
8395
+
8396
+ d: bytes | None = None
8397
+ o = self._cur_io
8398
+ while True:
8399
+ if o is None:
8400
+ try:
8401
+ if d is not None:
8402
+ o = coro.send(d)
8403
+ d = None
8404
+ else:
8405
+ o = next(coro)
8406
+ except StopIteration:
8407
+ self.close()
8408
+ o = None
8409
+ break
8410
+
8411
+ if isinstance(o, CoroHttpServer.AnyLogIo):
8412
+ if self._log_handler is not None:
8413
+ self._log_handler(self._coro_srv, o)
8414
+ o = None
8415
+
8416
+ elif isinstance(o, CoroHttpServer.ReadIo):
8417
+ if (d := self._read_buf.read(o.sz)) is None:
8418
+ break
8419
+ o = None
8420
+
8421
+ elif isinstance(o, CoroHttpServer.ReadLineIo):
8422
+ if (d := self._read_buf.read_until(b'\n')) is None:
8423
+ break
8424
+ o = None
8425
+
8426
+ elif isinstance(o, CoroHttpServer.WriteIo):
8427
+ check.none(self._write_buf)
8428
+ self._write_buf = IncrementalWriteBuffer(o.data, write_size=self._write_size)
8429
+ break
8430
+
8431
+ else:
8432
+ raise TypeError(o)
8433
+
8434
+ self._cur_io = o
8435
+
8436
+ #
8437
+
8438
+ def readable(self) -> bool:
8439
+ return True
8440
+
8441
+ def writable(self) -> bool:
8442
+ return self._write_buf is not None
8443
+
8444
+ #
8445
+
8446
+ def on_readable(self) -> None:
8447
+ try:
8448
+ buf = check.not_none(self._sock).recv(self._read_size)
8449
+ except BlockingIOError:
8450
+ return
8451
+ except ConnectionResetError:
8452
+ self.close()
8453
+ return
8454
+ if not buf:
8455
+ self.close()
8456
+ return
8457
+
8458
+ self._read_buf.feed(buf)
8459
+
8460
+ if isinstance(self._cur_io, CoroHttpServer.AnyReadIo):
8461
+ self._next_io()
8462
+
8463
+ def on_writable(self) -> None:
8464
+ check.isinstance(self._cur_io, CoroHttpServer.WriteIo)
8465
+ wb = check.not_none(self._write_buf)
8466
+ while wb.rem > 0:
8467
+ def send(d: bytes) -> int:
8468
+ try:
8469
+ return check.not_none(self._sock).send(d)
8470
+ except ConnectionResetError:
8471
+ self.close()
8472
+ return 0
8473
+ except BlockingIOError:
8474
+ return 0
8475
+ if not wb.write(send):
8476
+ break
8477
+
8478
+ if wb.rem < 1:
8479
+ self._write_buf = None
8480
+ self._cur_io = None
8481
+ self._next_io()
8482
+
8483
+
8088
8484
  ########################################
8089
8485
  # ../groups.py
8090
8486
 
@@ -9660,7 +10056,7 @@ def main(
9660
10056
 
9661
10057
  # if we hup, restart by making a new Supervisor()
9662
10058
  for epoch in itertools.count():
9663
- config = read_config_file(
10059
+ config = load_config_file_obj(
9664
10060
  os.path.expanduser(cf),
9665
10061
  ServerConfig,
9666
10062
  prepare=prepare_server_config,